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 +323 -102 1.40trunk 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'),
@@ -582,9 +745,10 @@
582 745 'is_installed' => defined('FLUENTCRM'),
583 746 'learn_more_url' => 'https://fluentcrm.com/',
584 747 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluentcrm/',
585 748 'action_text' => $this->isPluginInstalled('fluent-crm/fluent-crm.php') ? __('Activate FluentCRM', 'fluent-boards') : __('Install FluentCRM', 'fluent-boards'),
586 - 'description' => __('FluentCRM is a Self Hosted Email Marketing Automation Plugin for WordPress. Manage your leads and customers, email campaigns, automated email sequencing and many more', 'fluent-boards')
749 + 'description' => __('FluentCRM is a Self Hosted Email Marketing Automation Plugin for WordPress. Manage your leads and customers, email campaigns, automated email sequencing and many more', 'fluent-boards'),
750 + 'short_desc' => __('Email marketing automation', 'fluent-boards')
587 751 ],
588 752 'fluentform' => [
589 753 'title' => __('Fluent Forms', 'fluent-boards'),
590 754 'logo' => fluent_boards_mix('images/addons/fluentform.png'),
@@ -591,9 +755,10 @@
591 755 'is_installed' => defined('FLUENTFORM'),
592 756 'learn_more_url' => 'https://wordpress.org/plugins/fluentform/',
593 757 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluent-forms/',
594 758 'action_text' => $this->isPluginInstalled('fluent-form/fluent-form.php') ? __('Activate Fluent Forms', 'fluent-boards') : __('Install Fluent Forms', 'fluent-boards'),
595 - 'description' => __('Collect leads and build any type of forms, accept payments, connect with your CRM with the Fastest Contact Form Builder Plugin for WordPress', 'fluent-boards')
759 + 'description' => __('Collect leads and build any type of forms, accept payments, connect with your CRM with the Fastest Contact Form Builder Plugin for WordPress', 'fluent-boards'),
760 + 'short_desc' => __('Create forms and accept payments', 'fluent-boards')
596 761 ],
597 762 'fluent-support' => [
598 763 'title' => __('Fluent Support', 'fluent-boards'),
599 764 'logo' => fluent_boards_mix('images/addons/fluent-support.svg'),
@@ -601,9 +766,10 @@
601 766 'learn_more_url' => 'https://wordpress.org/plugins/fluent-connect/',
602 767 'settings_url' => admin_url('admin.php?page=fluent-support#/'),
603 768 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluentsupport/',
604 769 'action_text' => $this->isPluginInstalled('fluent-support/fluent-support.php') ? __('Activate Fluent Support', 'fluent-boards') : __('Install Fluent Support', 'fluent-boards'),
605 - 'description' => __('WordPress Helpdesk and Customer Support Ticket Plugin. Provide awesome support and manage customer queries right from your WordPress dashboard.', 'fluent-boards')
770 + 'description' => __('WordPress Helpdesk and Customer Support Ticket Plugin. Provide awesome support and manage customer queries right from your WordPress dashboard.', 'fluent-boards'),
771 + 'short_desc' => __('Customer support ticketing', 'fluent-boards')
606 772 ],
607 773 'fluent-smtp' => [
608 774 'title' => __('Fluent SMTP', 'fluent-boards'),
609 775 'logo' => fluent_boards_mix('images/addons/fluent-smtp.svg'),
@@ -610,10 +776,24 @@
610 776 'is_installed' => defined('FLUENTMAIL'),
611 777 'learn_more_url' => 'https://wordpress.org/plugins/fluent-smtp/',
612 778 'associate_doc' => admin_url('options-general.php?page=fluent-mail#/'),
613 779 'action_text' => $this->isPluginInstalled('fluent-smtp/fluent-smtp.php') ? __('Activate Fluent SMTP', 'fluent-boards') : __('Install Fluent SMTP', 'fluent-boards'),
614 - 'description' => __('The Ultimate SMTP and SES Plugin for WordPress. Connect with any SMTP, SendGrid, Mailgun, SES, Sendinblue, PepiPost, Google, Microsoft and more.', 'fluent-boards')
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'),
781 + 'short_desc' => __('Reliable email delivery with SMTP', 'fluent-boards')
615 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 + ],
616 796 ];
617 797
618 798 $addOns = apply_filters('fluent_boards/addons_settings', $addOns);
619 799
@@ -638,16 +818,27 @@
638 818 'message' => __('This feature is only available in Fluent Boards Pro', 'fluent-boards')
639 819 ]);
640 820 }
641 821
642 - $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 + }
643 830
644 831 $prefSettings = fluent_boards_get_pref_settings(false);
645 832
646 - $settings = wp_parse_args($settings, $prefSettings);
833 + $settings = wp_parse_args($rawSettings, $prefSettings);
647 834
648 835 $settings = Arr::only($settings, array_keys($prefSettings));
649 - $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 + }
650 841
651 842 if (empty($settings['frontend']['slug'])) {
652 843 $settings['frontend']['slug'] = 'projects';
653 844 }
@@ -717,8 +908,19 @@
717 908 {
718 909 return file_exists(WP_PLUGIN_DIR . '/' . $plugin);
719 910 }
720 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 +
721 923 private function backgroundInstaller($plugin_to_install, $plugin_id)
722 924 {
723 925 if (!empty($plugin_to_install['repo-slug'])) {
724 926 require_once ABSPATH . 'wp-admin/includes/file.php';
@@ -770,9 +972,9 @@
770 972 )
771 973 );
772 974
773 975 if (is_wp_error($plugin_information)) {
774 - throw new \Exception($plugin_information->get_error_message());
976 + throw new \Exception(esc_html($plugin_information->get_error_message()));
775 977 }
776 978
777 979 $package = $plugin_information->download_link;
778 980 $download = $upgrader->download_package($package);
@@ -777,15 +979,15 @@
777 979 $package = $plugin_information->download_link;
778 980 $download = $upgrader->download_package($package);
779 981
780 982 if (is_wp_error($download)) {
781 - throw new \Exception($download->get_error_message());
983 + throw new \Exception(esc_html($download->get_error_message()));
782 984 }
783 985
784 986 $working_dir = $upgrader->unpack_package($download, true);
785 987
786 988 if (is_wp_error($working_dir)) {
787 - throw new \Exception($working_dir->get_error_message());
989 + throw new \Exception(esc_html($working_dir->get_error_message()));
788 990 }
789 991
790 992 $result = $upgrader->install_package(
791 993 array(
@@ -801,9 +1003,9 @@
801 1003 )
802 1004 );
803 1005
804 1006 if (is_wp_error($result)) {
805 - throw new \Exception($result->get_error_message());
1007 + throw new \Exception(esc_html($result->get_error_message()));
806 1008 }
807 1009
808 1010 $activate = true;
809 1011
@@ -821,9 +1023,9 @@
821 1023 try {
822 1024 $result = activate_plugin($installed ? $installed_plugins[$plugin_file] : $plugin_slug . '/' . $plugin_file);
823 1025
824 1026 if (is_wp_error($result)) {
825 - throw new \Exception($result->get_error_message());
1027 + throw new \Exception(esc_html($result->get_error_message()));
826 1028 }
827 1029 } catch (\Exception $e) {
828 1030 }
829 1031 }
@@ -864,9 +1066,10 @@
864 1066 $pages = [];
865 1067 foreach ($allPages as $page) {
866 1068 $pages[] = [
867 1069 'id' => $page->ID,
868 - '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))
869 1072 ];
870 1073 }
871 1074
872 1075 return $this->sendSuccess([
@@ -892,9 +1095,25 @@
892 1095 return $this->sendError([
893 1096 'message' => __('This feature is only available in Fluent Boards Pro. Please upgrade.', 'fluent-boards')
894 1097 ]);
895 1098 }
896 - $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 + }
897 1116
898 1117 $settings = apply_filters('fluent_boards/save_general_settings', $settings);
899 1118
900 1119 $savedSettings = fluent_boards_update_option('general_settings', $settings);
@@ -901,9 +1120,11 @@
901 1120 $savedGeneralSettings = \maybe_unserialize($savedSettings->value);
902 1121
903 1122 $scheduleHandler = new ProScheduleHandler();
904 1123
905 - 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)) {
906 1127 // force schedule from this settings update
907 1128 $scheduleHandler->clearDailyTaskReminderScheduler();
908 1129 $scheduleHandler->scheduleDailyTaskReminder();
909 1130 }