PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / trunk
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration vtrunk
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 1.45 All 41 releases
← All changes | app/Http/Controllers/OptionsController.php +315 -98 1.45trunk View file →
@@ -8,8 +8,9 @@
8 8 use FluentBoards\App\Models\User;
9 9 use FluentBoards\App\Models\Board;
10 10 use FluentBoards\App\Models\Relation;
11 11 use FluentBoards\App\Services\BoardService;
12 +use FluentBoards\App\Services\DescriptionMarkdownConverter;
12 13 use FluentBoards\App\Services\Helper;
13 14 use FluentBoards\App\Services\OptionService;
14 15 use FluentBoards\App\Services\Constant;
15 16 use FluentBoards\App\Services\PermissionManager;
@@ -31,18 +32,18 @@
31 32
32 33 public function selectorOptions(Request $request)
33 34 {
34 35 try {
35 - $optionKey = $request->getSafe('option_key');
36 - $search = $request->getSafe('search');
36 + $optionKey = $request->getSafe('option_key', 'sanitize_text_field');
37 + $search = $request->getSafe('search', 'sanitize_text_field');
37 38 $includedIds = $request->getSafe('values');
38 - $boardId = $request->getSafe('board_id');
39 + $boardId = $request->getSafe('board_id', 'intval');
39 40
40 41 $options = [];
41 42 if ('users' === $optionKey || 'task_assignees' === $optionKey) { // no ajax/code is designed to handle this eventually will goto else
42 43
43 44 if (!PermissionManager::isBoardManager($boardId)) {
44 - throw new \Exception('You do not have permission to access this route');
45 + throw new \Exception(esc_html__('You do not have permission to access this route', 'fluent-boards'));
45 46 }
46 47
47 48 if (!defined('FLUENT_BOARDS_PRO')) {
48 49 // get who has 'manage_options' capability
@@ -56,11 +57,30 @@
56 57 }
57 58
58 59 $options = $this->addUserDataAsSelectorOption($users);
59 60
61 + } elseif ('board_create_users' === $optionKey) {
62 + if (!PermissionManager::userHasBoardCreationPermission()) {
63 + throw new \Exception(esc_html__('You do not have permission to access this route', 'fluent-boards'));
64 + }
65 +
66 + if (!defined('FLUENT_BOARDS_PRO')) {
67 + // Bound the lookup: this popover searches on focus (empty query too).
68 + $users = PermissionManager::getAll_WP_Admins($search, 20);
69 + } else {
70 + $users = Helper::searchWordPressUsers($search);
71 + }
72 +
73 + $options = $this->addUserDataAsSelectorOption($users);
74 + // Board creation can be delegated to members without `list_users`;
75 + // don't leak full account emails to them.
76 + $options = $this->maskSelectorEmailsForViewer($options);
77 +
60 78 } elseif ('boards' === $optionKey) {
61 79 $boards = Board::query()
80 + ->byAccessUser(get_current_user_id())
62 81 ->when($search, function ($query) use ($search) {
82 + // $search is already sanitized with sanitize_text_field above
63 83 return $query->where('title', 'LIKE', '%' . $search . '%');
64 84 })->take(20)->get();
65 85
66 86 foreach ($boards as $board) {
@@ -71,12 +91,18 @@
71 91 'right_side_value' => $board->slug,
72 92 ];
73 93 }
74 94 } elseif ('tasks' === $optionKey) {
95 + if (!PermissionManager::userHasPermission($boardId)) {
96 + throw new \Exception(esc_html__('You do not have permission to access this route', 'fluent-boards'));
97 + }
98 +
99 + // $boardId is already sanitized with intval above
75 100 $tasks = Task::where('board_id', $boardId)
76 101 ->whereNull('archived_at')
77 102 ->whereNull('parent_id')
78 103 ->when($search, function ($query) use ($search) {
104 + // $search is already sanitized with sanitize_text_field above
79 105 return $query->where('title', 'LIKE', '%' . $search . '%');
80 106 })->take(20)->get();
81 107
82 108 foreach ($tasks as $task) {
@@ -82,13 +108,18 @@
82 108 foreach ($tasks as $task) {
83 109 $options[] = [
84 110 'id' => $task->id,
85 111 'title' => $task->title,
86 - 'board_id' => $task->board_id
112 + 'board_id' => $task->board_id,
113 + 'subtask_groups' => $task->subtaskGroup
87 114 ];
88 115 }
89 116
90 117 } elseif ('assigned_in_task' == $optionKey) {
118 + if (!PermissionManager::userHasPermission($boardId)) {
119 + throw new \Exception(esc_html__('You do not have permission to access this route', 'fluent-boards'));
120 + }
121 +
91 122 $users = (new BoardService())->getAssigneesByBoard($boardId, $search);
92 123 $options = $this->addUserDataAsSelectorOption($users);
93 124 } else {
94 125 $options = apply_filters('fluent_boards/ajax_options_' . $optionKey, [], $search, $includedIds);
@@ -116,8 +147,42 @@
116 147 }
117 148 return $options;
118 149 }
119 150
151 + /**
152 + * Obfuscate emails in selector options for viewers who lack the `list_users`
153 + * capability, keeping the current user's own email visible.
154 + */
155 + private function maskSelectorEmailsForViewer($options)
156 + {
157 + if (current_user_can('list_users')) {
158 + return $options;
159 + }
160 +
161 + $currentUser = wp_get_current_user();
162 + $currentUserEmail = ($currentUser && isset($currentUser->user_email)) ? $currentUser->user_email : '';
163 +
164 + foreach ($options as $index => $option) {
165 + $email = $option['email'] ?? '';
166 +
167 + if ($email === '' || $email === $currentUserEmail) {
168 + continue;
169 + }
170 +
171 + $maskedEmail = Helper::obfuscateEmail($email);
172 +
173 + // When there's no display name the raw email doubles as the name.
174 + if (($option['name'] ?? '') === $email) {
175 + $options[$index]['name'] = $maskedEmail;
176 + }
177 +
178 + $options[$index]['email'] = $maskedEmail;
179 + $options[$index]['title'] = ($options[$index]['name'] ?? $maskedEmail) . ' (' . $maskedEmail . ')';
180 + }
181 +
182 + return $options;
183 + }
184 +
120 185 public function getCurrentUserPermissions()
121 186 {
122 187 try {
123 188 $currentUserBoards = Relation::query()
@@ -139,10 +204,10 @@
139 204
140 205 public function getUserPermission(Request $request)
141 206 {
142 207 try {
143 - $boardId = $request->getSafe('boardId');
144 - $userId = $request->getSafe('userId');
208 + $boardId = $request->getSafe('boardId', 'intval');
209 + $userId = $request->getSafe('userId', 'intval');
145 210
146 211 $boardUser = Relation::where('board_id', $boardId)
147 212 ->where('user_id', $userId)
148 213 ->where('status', 'ACTIVE')->first();
@@ -160,12 +225,12 @@
160 225
161 226 public function updatedUserPermission(Request $request)
162 227 {
163 228 try {
164 - $permission = $request->getSafe('userPermission');
165 - $updateType = $request->getSafe('updateType');
166 - $boardId = $request->getSafe('boardId');
167 - $userId = $request->getSafe('userId');
229 + $permission = $request->getSafe('userPermission', 'sanitize_text_field');
230 + $updateType = $request->getSafe('updateType', 'sanitize_text_field');
231 + $boardId = $request->getSafe('boardId', 'intval');
232 + $userId = $request->getSafe('userId', 'intval');
168 233
169 234 $boardUser = Relation::where('board_id', $boardId)->where('user_id', $userId)->status('ACTIVE')->first();
170 235
171 236 if ('Board Admin' == $permission) {
@@ -197,8 +262,9 @@
197 262 }
198 263
199 264 public function SetUserSuperAdmin($userId)
200 265 {
266 + $userId = absint($userId);
201 267 try {
202 268 $this->optionService->createSuperAdmin($userId);
203 269 return $this->sendSuccess([
204 270 'message' => __('Member has been set super admin successfully!', 'fluent-boards')
@@ -210,8 +276,9 @@
210 276 }
211 277
212 278 public function removeUserSuperAdmin($userId)
213 279 {
280 + $userId = absint($userId);
214 281 try {
215 282 $this->optionService->removeUserSuperAdmin($userId);
216 283
217 284 return $this->sendSuccess([
@@ -224,9 +291,9 @@
224 291
225 292 public function IsUserAllBoardAdmin(Request $request)
226 293 {
227 294 try {
228 - $userId = $request->getSafe('id');
295 + $userId = $request->getSafe('id', 'intval');
229 296 $isSuperAdmin = false;
230 297 $superAdmin = Relation::where('board_id', null)->where('user_id', $userId)->where('status', 'ACTIVE')->first();
231 298 $totalSuperAdmin = Relation::where('board_id', null)->where('status', 'ACTIVE')->count();
232 299 $permissions = [];
@@ -246,10 +313,11 @@
246 313 }
247 314
248 315 public function RemoveUserFromSuperAdmin(Request $request, $id)
249 316 {
317 + $id = absint($id);
250 318 try {
251 - $userId = $request->getSafe('id');
319 + $userId = $request->getSafe('id', 'intval');
252 320
253 321 $superAdmin = Relation::where('board_id', null)->where('user_id', $userId)->first();
254 322 $superAdmin->status = 'INACTIVE';
255 323 $superAdmin->save();
@@ -264,10 +332,10 @@
264 332
265 333 public function removeUserFromBoard(Request $request)
266 334 {
267 335 try {
268 - $boardId = $request->getSafe('boardId');
269 - $userId = $request->getSafe('userId');
336 + $boardId = $request->getSafe('boardId', 'intval');
337 + $userId = $request->getSafe('userId', 'intval');
270 338
271 339 $this->boardService->removeUserFromBoard($boardId, $userId);
272 340
273 341 if (!PermissionManager::isAdmin($userId)) {
@@ -284,9 +352,14 @@
284 352
285 353 public function addAsSuperAdmin(Request $request)
286 354 {
287 355 try {
288 - $userIds = $request->getSafe('memberIds');
356 + $rawUserIds = $request->getSafe('memberIds');
357 + // Sanitize array of user IDs
358 + $userIds = [];
359 + if (is_array($rawUserIds)) {
360 + $userIds = array_filter(array_map('intval', $rawUserIds));
361 + }
289 362 foreach ($userIds as $userId) {
290 363 $this->createSuperAdmin($userId);
291 364 }
292 365
@@ -315,10 +388,20 @@
315 388
316 389 public function addMembersInBoards(Request $request)
317 390 {
318 391 try {
319 - $userIds = $request->getSafe('memberIds');
320 - $boardIds = $request->getSafe('boardIds');
392 + $rawUserIds = $request->getSafe('memberIds');
393 + $rawBoardIds = $request->getSafe('boardIds');
394 +
395 + // Sanitize arrays of IDs
396 + $userIds = [];
397 + if (is_array($rawUserIds)) {
398 + $userIds = array_filter(array_map('intval', $rawUserIds));
399 + }
400 + $boardIds = [];
401 + if (is_array($rawBoardIds)) {
402 + $boardIds = array_filter(array_map('intval', $rawBoardIds));
403 + }
321 404
322 405 foreach ($userIds as $userId) {
323 406 foreach ($boardIds as $boardId) {
324 407 $this->boardService->addMembersInBoard($boardId, $userId);
@@ -336,9 +419,18 @@
336 419
337 420 public function updateGlobalNotificationSettings(Request $request)
338 421 {
339 422 try {
340 - $newSettings = $request->getSafe('updatedSettings');
423 + // updatedSettings is an array, sanitize each element
424 + $rawSettings = $request->get('updatedSettings');
425 + $newSettings = [];
426 + if (is_array($rawSettings)) {
427 + foreach ($rawSettings as $key => $value) {
428 + $sanitizedKey = sanitize_text_field($key);
429 + $sanitizedValue = sanitize_text_field($value);
430 + $newSettings[$sanitizedKey] = $sanitizedValue;
431 + }
432 + }
341 433
342 434 $this->optionService->updateGlobalNotificationSettings($newSettings);
343 435
344 436 return $this->sendSuccess([
@@ -373,21 +465,29 @@
373 465 }
374 466
375 467 $boardId = $request->getSafe('boardId', 'intval');
376 468
377 - $memberUserIds = Relation::where('object_type', 'board_user')
469 + if ($boardId && !PermissionManager::userHasPermission($boardId)) {
470 + return $this->sendError([
471 + 'message' => __('You do not have permission to access this route', 'fluent-boards')
472 + ], 403);
473 + }
474 +
475 + $memberUserIdsQuery = Relation::where('object_type', Constant::OBJECT_TYPE_BOARD_USER)
378 476 ->select(['foreign_id'])
379 477 ->groupBy('foreign_id');
380 478
381 479 if ($boardId) {
382 - $memberUserIds = $memberUserIds->where('object_id', $boardId);
480 + $memberUserIdsQuery->where('object_id', $boardId);
481 + } elseif (!PermissionManager::isAdmin()) {
482 + $boardIds = array_filter(array_map('intval', PermissionManager::getBoardIdsForUser()));
483 + $memberUserIdsQuery->whereIn('object_id', $boardIds);
383 484 }
384 485
385 - $members = [];
386 -
387 - $memberUserIds = $memberUserIds->get()
486 + $memberUserIds = $memberUserIdsQuery->get()
388 487 ->pluck('foreign_id')->toArray();
389 488
489 + $members = [];
390 490
391 491 if ($memberUserIds) {
392 492 $memberUsers = get_users([
393 493 'include' => $memberUserIds
@@ -437,92 +537,105 @@
437 537 'members' => $members
438 538 ];
439 539 }
440 540
441 - public function quickSearch()
541 + public function globalSearch()
442 542 {
443 543 $currentUserId = get_current_user_id();
444 544
445 - $query = sanitize_text_field($_REQUEST['query']);
446 - $query = strtolower($query);
447 - $scope = sanitize_text_field($_REQUEST['scope']);
545 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
546 + $query = isset($_REQUEST['query']) ? strtolower(sanitize_text_field(wp_unslash($_REQUEST['query']))) : '';
547 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
548 + $scope = isset($_REQUEST['scope']) ? sanitize_text_field(wp_unslash($_REQUEST['scope'])) : 'all';
448 549
550 + // Pagination parameters
551 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
552 + $taskPage = isset($_REQUEST['task_page']) ? max(1, (int)$_REQUEST['task_page']) : 0;
553 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
554 + $boardPage = isset($_REQUEST['board_page']) ? max(1, (int)$_REQUEST['board_page']) : 0;
555 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
556 + $perPage = isset($_REQUEST['per_page']) ? (int)$_REQUEST['per_page'] : 20;
557 + $perPage = max(1, min(100, $perPage));
558 +
559 + // Build base queries
449 560 $firstThreeChars = substr($query, 0, 3);
450 561 $firstNineChars = substr($query, 0, 9);
451 562
452 563 if($firstThreeChars == 'id:') {
453 - $idPart = substr($query, 3);
454 - $idPart = preg_replace('/[^a-zA-Z0-9]/', '', $idPart);
564 + $idPart = preg_replace('/[^a-zA-Z0-9]/', '', substr($query, 3));
455 565 $tasksQuery = Task::query()->where('parent_id', null)->whereRaw('id LIKE ?', ['%' . $idPart . '%']);
566 + $boardQuery = Board::query()->whereRaw('id LIKE ?', ['%' . $idPart . '%']);
456 567 }elseif($firstNineChars == 'archived:') {
457 - $archivedPart = substr($query, 9);
458 - $archivedPart = trim($archivedPart);
568 + $archivedPart = trim(substr($query, 9));
459 569 $tasksQuery = Task::query()->where('parent_id', null)->whereNotNull('archived_at')->whereRaw('LOWER(title) LIKE ?', ['%' . $archivedPart . '%']);
570 + $boardQuery = Board::query()->whereNotNull('archived_at')->whereRaw('LOWER(title) LIKE ?', ['%' . $archivedPart . '%']);
460 571 } else {
461 572 $tasksQuery = Task::query()->where('parent_id', null)->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']);
573 + $boardQuery = Board::query()->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']);
462 574 }
463 575
576 + // Apply scope and permissions
464 577 $isUserAdmin = PermissionManager::isAdmin($currentUserId);
465 -
466 - // This is a check for deleted tasks to be excluded from search results
467 - $allActiveBoardsIds = Board::query()->where('archived_at', null)->pluck('id')->toArray();
468 -
469 578 if ($scope == 'all') {
470 - if($firstThreeChars == 'id:'){
471 - $idPart = substr($query, 3);
472 - $idPart = preg_replace('/[^a-zA-Z0-9]/', '', $idPart);
473 - $boardQuery = Board::query()->whereRaw('id LIKE ?', ['%' . $idPart . '%']);
474 - }elseif ($firstNineChars == 'archived:') {
475 - $archivedPart = substr($query, 9);
476 - $archivedPart = trim($archivedPart);
477 - $boardQuery = Board::query()->whereNotNull('archived_at')->whereRaw('LOWER(title) LIKE ?', ['%' . $archivedPart . '%']);
478 - } else {
479 - $boardQuery = Board::query()->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']);
480 - }
481 - if ($isUserAdmin) {
482 - $boards = $boardQuery->limit(10)->get();
483 - $tasks = $tasksQuery->limit(10)->get();
484 - } else {
579 + if (!$isUserAdmin) {
485 580 $boardIds = PermissionManager::getBoardIdsForUser($currentUserId);
486 - $boards = $boardQuery->whereIn('id', $boardIds)->limit(10)->get();
487 - $tasks = $tasksQuery->whereIn('board_id', $boardIds)->limit(10)->get();
581 + $boardQuery->whereIn('id', $boardIds);
582 + $tasksQuery->whereIn('board_id', $boardIds);
488 583 }
489 584 } else {
490 - $boards = []; // boards results is not needed in scoped search
491 - $inBoard = (int)$scope;
492 - if ($isUserAdmin || in_array($inBoard, $boardIds = PermissionManager::getBoardIdsForUser($currentUserId))) {
493 - $tasks = $tasksQuery->where('board_id', $inBoard)->limit(10)->get();
585 + // For 'current_board' scope, we don't search boards
586 + $boardQuery->where('id', -1);
587 +
588 + $inBoard = absint($scope);
589 + if ($isUserAdmin || in_array($inBoard, PermissionManager::getBoardIdsForUser($currentUserId))) {
590 + $tasksQuery->where('board_id', $inBoard);
494 591 } else {
495 - // Out of permission scope search
496 - $tasks = [];
592 + $tasksQuery->where('id', -1); // Force no results
497 593 }
498 594 }
499 595
596 + $allActiveBoardsIds = Board::query()->where('archived_at', null)->pluck('id')->toArray();
500 597
598 + $boards = [];
599 + $tasks = [];
600 + $totalBoards = 0;
601 + $totalTasks = 0;
602 + $formattedBoards = [];
501 603 $formattedTasks = [];
502 - $formattedBoards = [];
604 +
605 + // Fetch Boards if requested
606 + if ($boardPage > 0) {
607 + $totalBoards = $boardQuery->count();
608 + $boardOffset = ($boardPage - 1) * $perPage;
609 + $boards = $boardQuery->skip($boardOffset)->take($perPage)->get();
610 + }
611 +
612 + // Fetch Tasks if requested
613 + if ($taskPage > 0) {
614 + $totalTasks = $tasksQuery->count();
615 + $taskOffset = ($taskPage - 1) * $perPage;
616 + $tasks = $tasksQuery->skip($taskOffset)->take($perPage)->get();
617 + }
618 +
503 619 foreach ($boards as $board) {
504 620 $formattedBoards[] = [
505 621 'type' => 'board',
506 622 'id' => $board->id,
507 623 'title' => $board->title,
508 - 'description' => $board->description,
624 + 'description' => DescriptionMarkdownConverter::normalize($board->description),
509 625 ];
510 626 }
511 627 foreach ($tasks as $task) {
512 -
513 628 if (!in_array($task->board_id, $allActiveBoardsIds)) {
514 - // if the task is not in an active board, skip it
515 629 continue;
516 630 }
517 631
518 632 $board = $task->board;
519 -
520 633 $formattedTasks[] = [
521 634 'type' => 'task',
522 635 'id' => $task->id,
523 636 'title' => $task->title,
524 - 'description' => $task->description,
637 + 'description' => DescriptionMarkdownConverter::normalize($task->description),
525 638 'board_id' => $task->board_id,
526 639 'board' => [
527 640 'id' => $board->id,
528 641 'title' => $board->title,
@@ -531,51 +644,101 @@
531 644 'stage' => [
532 645 'id' => $task->stage_id,
533 646 'title' => $task->stage->title ?? '',
534 647 ],
535 -
536 648 ];
537 649 }
650 +
538 651 return $this->sendSuccess([
539 - 'tasks' => $formattedTasks,
540 - 'boards' => $formattedBoards
652 + 'tasks' => [
653 + 'data' => $formattedTasks,
654 + 'current_page' => $taskPage,
655 + 'per_page' => $perPage,
656 + 'total' => $totalTasks,
657 + 'last_page' => (int) ceil($totalTasks / $perPage)
658 + ],
659 + 'boards' => [
660 + 'data' => $formattedBoards,
661 + 'current_page' => $boardPage,
662 + 'per_page' => $perPage,
663 + 'total' => $totalBoards,
664 + 'last_page' => (int) ceil($totalBoards / $perPage)
665 + ]
541 666 ], 200);
542 -
543 667 }
544 668
545 - public function getDashboardViewSettings()
669 + public function getDashboardViewSettings(Request $request)
546 670 {
547 - try {
671 + $view = $request->getSafe('view', 'sanitize_text_field');
672 +
673 + if ($view == 'kanbanview') {
548 674 $globalSettings = $this->optionService->getDashboardViewSettings();
549 - if ($globalSettings->value)
550 - $currentSettings = maybe_unserialize($globalSettings->value);
675 + } elseif ($view == 'listview') {
676 + $globalSettings = $this->optionService->getListViewPreferences();
677 + } elseif ($view == 'tableview') {
678 + $globalSettings = $this->optionService->getTableViewPreferences();
679 + } else {
680 + // Handle invalid view or default to one
681 + return $this->sendError(['message' => __('Invalid view type', 'fluent-boards')], 400);
682 + }
551 683
552 - return $this->sendSuccess([
553 - 'currentSettings' => $currentSettings,
554 - ], 200);
555 - } catch (\Exception $e) {
556 - return $this->sendError($e->getMessage(), 404);
557 - }
684 + if ($globalSettings->value)
685 + $currentSettings = maybe_unserialize($globalSettings->value);
686 +
687 + return $this->sendSuccess([
688 + 'currentSettings' => $currentSettings,
689 + ], 200);
558 690 }
559 691
560 692 public function updateDashboardViewSettings(Request $request)
561 693 {
562 - try {
563 - $newSettings = $request->getSafe('updatedSettings');
694 + // updatedSettings is an array, sanitize each element
695 + $rawSettings = $request->get('updatedSettings');
696 + $newSettings = [];
697 + if (is_array($rawSettings)) {
698 + foreach ($rawSettings as $key => $value) {
699 + $sanitizedKey = sanitize_text_field($key);
700 + // Value could be string, boolean, or number - sanitize appropriately
701 + if (is_string($value)) {
702 + $sanitizedValue = sanitize_text_field($value);
703 + } elseif (is_bool($value) || is_numeric($value)) {
704 + $sanitizedValue = $value;
705 + } else {
706 + $sanitizedValue = sanitize_text_field((string)$value);
707 + }
708 + $newSettings[$sanitizedKey] = $sanitizedValue;
709 + }
710 + }
711 + $view = $request->getSafe('view', 'sanitize_text_field');
564 712
565 - $this->optionService->updateDashboardViewSettings($newSettings);
713 + $this->optionService->updateDashboardViewSettings($newSettings, $view);
566 714
567 - return $this->sendSuccess([
568 - 'message' => __("Dashboard view settings are updated", 'fluent-boards'),
569 - ], 201);
570 - } catch (\Exception $e) {
571 - return $this->sendError($e->getMessage(), 404);
715 + if ($view == 'listview') {
716 + $message = __("List view settings updated successfully", 'fluent-boards');
717 + } elseif ($view == 'tableview') {
718 + $message = __("Table view settings updated successfully", 'fluent-boards');
719 + } else {
720 + $message = __("Card view settings updated successfully", 'fluent-boards');
572 721 }
722 +
723 + return $this->sendSuccess([
724 + 'message' => $message,
725 + ], 201);
573 726 }
574 727
575 728
576 729 public function getAddonsSettings()
577 730 {
731 + $canAutoInstallKit = $this->canAutoInstallFluentKit();
732 + $kitPluginFile = 'fluent-toolkit/fluent-toolkit.php';
733 + $kitLoaded = defined('FLUENT_TOOLKIT_VERSION');
734 + $kitPluginExists = $this->isPluginInstalled($kitPluginFile);
735 + $kitActionText = __('Get FluentHub from GitHub', 'fluent-boards');
736 +
737 + if ($canAutoInstallKit) {
738 + $kitActionText = $kitPluginExists ? __('Activate FluentHub', 'fluent-boards') : __('Install FluentHub', 'fluent-boards');
739 + }
740 +
578 741 $addOns = [
579 742 'fluent-crm' => [
580 743 'title' => __('FluentCRM', 'fluent-boards'),
581 744 'logo' => fluent_boards_mix('images/addons/fluent-crm.svg'),
@@ -616,8 +779,21 @@
616 779 'action_text' => $this->isPluginInstalled('fluent-smtp/fluent-smtp.php') ? __('Activate Fluent SMTP', 'fluent-boards') : __('Install Fluent SMTP', 'fluent-boards'),
617 780 'description' => __('The Ultimate SMTP and SES Plugin for WordPress. Connect with any SMTP, SendGrid, Mailgun, SES, Sendinblue, PepiPost, Google, Microsoft and more.', 'fluent-boards'),
618 781 'short_desc' => __('Reliable email delivery with SMTP', 'fluent-boards')
619 782 ],
783 + 'fluent-toolkit' => [
784 + 'title' => __('FluentHub', 'fluent-boards'),
785 + 'logo' => fluent_boards_mix('images/addons/fluent-toolkit.svg'),
786 + 'is_installed' => $kitLoaded,
787 + 'learn_more_url' => 'https://github.com/WPManageNinja/fluent-toolkit',
788 + 'settings_url' => admin_url('admin.php?page=fluent-toolkit'),
789 + 'associate_doc' => 'https://github.com/WPManageNinja/fluent-toolkit',
790 + 'action_text' => $kitActionText,
791 + 'install_route' => $canAutoInstallKit ? 'admin/mcp/install-adapter' : '',
792 + 'install_url' => $canAutoInstallKit ? '' : 'https://github.com/WPManageNinja/fluent-toolkit',
793 + 'description' => __('Fluent Boards MCP tools become available after FluentHub is installed and active.', 'fluent-boards'),
794 + 'short_desc' => __('AI agent tools for Fluent Boards', 'fluent-boards')
795 + ],
620 796 ];
621 797
622 798 $addOns = apply_filters('fluent_boards/addons_settings', $addOns);
623 799
@@ -642,16 +818,27 @@
642 818 'message' => __('This feature is only available in Fluent Boards Pro', 'fluent-boards')
643 819 ]);
644 820 }
645 821
646 - $settings = $request->get('settings', []);
822 + $rawSettings = $request->get('settings', []);
823 +
824 + // Validate that settings is an array
825 + if (!is_array($rawSettings)) {
826 + return $this->sendError([
827 + 'message' => __('Invalid settings format', 'fluent-boards')
828 + ], 400);
829 + }
647 830
648 831 $prefSettings = fluent_boards_get_pref_settings(false);
649 832
650 - $settings = wp_parse_args($settings, $prefSettings);
833 + $settings = wp_parse_args($rawSettings, $prefSettings);
651 834
652 835 $settings = Arr::only($settings, array_keys($prefSettings));
653 - $settings['frontend']['slug'] = sanitize_title($settings['frontend']['slug']);
836 +
837 + // Sanitize slug if it exists
838 + if (isset($settings['frontend']['slug'])) {
839 + $settings['frontend']['slug'] = sanitize_title($settings['frontend']['slug']);
840 + }
654 841
655 842 if (empty($settings['frontend']['slug'])) {
656 843 $settings['frontend']['slug'] = 'projects';
657 844 }
@@ -721,8 +908,19 @@
721 908 {
722 909 return file_exists(WP_PLUGIN_DIR . '/' . $plugin);
723 910 }
724 911
912 + private function canAutoInstallFluentKit()
913 + {
914 + $canAutoInstall = (bool) apply_filters('fluent_kit/can_auto_install', false);
915 +
916 + if (!$canAutoInstall) {
917 + $canAutoInstall = (bool) apply_filters('fluent_toolkit/can_auto_install', false);
918 + }
919 +
920 + return $canAutoInstall;
921 + }
922 +
725 923 private function backgroundInstaller($plugin_to_install, $plugin_id)
726 924 {
727 925 if (!empty($plugin_to_install['repo-slug'])) {
728 926 require_once ABSPATH . 'wp-admin/includes/file.php';
@@ -774,9 +972,9 @@
774 972 )
775 973 );
776 974
777 975 if (is_wp_error($plugin_information)) {
778 - throw new \Exception($plugin_information->get_error_message());
976 + throw new \Exception(esc_html($plugin_information->get_error_message()));
779 977 }
780 978
781 979 $package = $plugin_information->download_link;
782 980 $download = $upgrader->download_package($package);
@@ -781,15 +979,15 @@
781 979 $package = $plugin_information->download_link;
782 980 $download = $upgrader->download_package($package);
783 981
784 982 if (is_wp_error($download)) {
785 - throw new \Exception($download->get_error_message());
983 + throw new \Exception(esc_html($download->get_error_message()));
786 984 }
787 985
788 986 $working_dir = $upgrader->unpack_package($download, true);
789 987
790 988 if (is_wp_error($working_dir)) {
791 - throw new \Exception($working_dir->get_error_message());
989 + throw new \Exception(esc_html($working_dir->get_error_message()));
792 990 }
793 991
794 992 $result = $upgrader->install_package(
795 993 array(
@@ -805,9 +1003,9 @@
805 1003 )
806 1004 );
807 1005
808 1006 if (is_wp_error($result)) {
809 - throw new \Exception($result->get_error_message());
1007 + throw new \Exception(esc_html($result->get_error_message()));
810 1008 }
811 1009
812 1010 $activate = true;
813 1011
@@ -825,9 +1023,9 @@
825 1023 try {
826 1024 $result = activate_plugin($installed ? $installed_plugins[$plugin_file] : $plugin_slug . '/' . $plugin_file);
827 1025
828 1026 if (is_wp_error($result)) {
829 - throw new \Exception($result->get_error_message());
1027 + throw new \Exception(esc_html($result->get_error_message()));
830 1028 }
831 1029 } catch (\Exception $e) {
832 1030 }
833 1031 }
@@ -868,9 +1066,10 @@
868 1066 $pages = [];
869 1067 foreach ($allPages as $page) {
870 1068 $pages[] = [
871 1069 'id' => $page->ID,
872 - 'title' => $page->post_title ? $page->post_title : __('(no title)', 'fluent-boards')
1070 + 'title' => $page->post_title ? $page->post_title : __('(no title)', 'fluent-boards'),
1071 + 'url' => esc_url_raw(get_permalink($page->ID))
873 1072 ];
874 1073 }
875 1074
876 1075 return $this->sendSuccess([
@@ -896,9 +1095,25 @@
896 1095 return $this->sendError([
897 1096 'message' => __('This feature is only available in Fluent Boards Pro. Please upgrade.', 'fluent-boards')
898 1097 ]);
899 1098 }
900 - $settings = $request->getSafe('updatedSettings', []);
1099 + // updatedSettings is an array, sanitize each element
1100 + $rawSettings = $request->get('updatedSettings', []);
1101 + $settings = [];
1102 + if (is_array($rawSettings)) {
1103 + foreach ($rawSettings as $key => $value) {
1104 + $sanitizedKey = sanitize_text_field($key);
1105 + // Value could be string, boolean, or number - sanitize appropriately
1106 + if (is_string($value)) {
1107 + $sanitizedValue = sanitize_text_field($value);
1108 + } elseif (is_bool($value) || is_numeric($value)) {
1109 + $sanitizedValue = $value;
1110 + } else {
1111 + $sanitizedValue = sanitize_text_field((string)$value);
1112 + }
1113 + $settings[$sanitizedKey] = $sanitizedValue;
1114 + }
1115 + }
901 1116
902 1117 $settings = apply_filters('fluent_boards/save_general_settings', $settings);
903 1118
904 1119 $savedSettings = fluent_boards_update_option('general_settings', $settings);
@@ -905,9 +1120,11 @@
905 1120 $savedGeneralSettings = \maybe_unserialize($savedSettings->value);
906 1121
907 1122 $scheduleHandler = new ProScheduleHandler();
908 1123
909 - if ($savedGeneralSettings['daily_reminder_enabled'] || $savedGeneralSettings['daily_reminder_enabled'] == 'true') {
1124 + $dailyReminderEnabled = $savedGeneralSettings['daily_reminder_enabled'] ?? false;
1125 +
1126 + if (filter_var($dailyReminderEnabled, FILTER_VALIDATE_BOOLEAN)) {
910 1127 // force schedule from this settings update
911 1128 $scheduleHandler->clearDailyTaskReminderScheduler();
912 1129 $scheduleHandler->scheduleDailyTaskReminder();
913 1130 }