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 +329 -105 1.30trunk 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,94 +537,106 @@
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->get();
483 - $tasks = $tasksQuery->get();
484 - } else {
579 + if (!$isUserAdmin) {
485 580 $boardIds = PermissionManager::getBoardIdsForUser($currentUserId);
486 - $boards = $boardQuery->whereIn('id', $boardIds)->get();
487 - $tasks = $tasksQuery->whereIn('board_id', $boardIds)->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)->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,
509 - 'url' => Helper::getBoardUrl($board->id)
624 + 'description' => DescriptionMarkdownConverter::normalize($board->description),
510 625 ];
511 626 }
512 627 foreach ($tasks as $task) {
513 -
514 628 if (!in_array($task->board_id, $allActiveBoardsIds)) {
515 - // if the task is not in an active board, skip it
516 629 continue;
517 630 }
518 631
519 632 $board = $task->board;
520 -
521 633 $formattedTasks[] = [
522 634 'type' => 'task',
523 635 'id' => $task->id,
524 636 'title' => $task->title,
525 - 'description' => $task->description,
526 - 'url' => Helper::getTaskUrl($task->id, $board->id),
637 + 'description' => DescriptionMarkdownConverter::normalize($task->description),
638 + 'board_id' => $task->board_id,
527 639 'board' => [
528 640 'id' => $board->id,
529 641 'title' => $board->title,
530 642 'url' => Helper::getBoardUrl($board->id)
@@ -532,51 +644,101 @@
532 644 'stage' => [
533 645 'id' => $task->stage_id,
534 646 'title' => $task->stage->title ?? '',
535 647 ],
536 -
537 648 ];
538 649 }
650 +
539 651 return $this->sendSuccess([
540 - 'tasks' => $formattedTasks,
541 - '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 + ]
542 666 ], 200);
543 -
544 667 }
545 668
546 - public function getDashboardViewSettings()
669 + public function getDashboardViewSettings(Request $request)
547 670 {
548 - try {
671 + $view = $request->getSafe('view', 'sanitize_text_field');
672 +
673 + if ($view == 'kanbanview') {
549 674 $globalSettings = $this->optionService->getDashboardViewSettings();
550 - if ($globalSettings->value)
551 - $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 + }
552 683
553 - return $this->sendSuccess([
554 - 'currentSettings' => $currentSettings,
555 - ], 200);
556 - } catch (\Exception $e) {
557 - return $this->sendError($e->getMessage(), 404);
558 - }
684 + if ($globalSettings->value)
685 + $currentSettings = maybe_unserialize($globalSettings->value);
686 +
687 + return $this->sendSuccess([
688 + 'currentSettings' => $currentSettings,
689 + ], 200);
559 690 }
560 691
561 692 public function updateDashboardViewSettings(Request $request)
562 693 {
563 - try {
564 - $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');
565 712
566 - $this->optionService->updateDashboardViewSettings($newSettings);
713 + $this->optionService->updateDashboardViewSettings($newSettings, $view);
567 714
568 - return $this->sendSuccess([
569 - 'message' => __("Dashboard view settings are updated", 'fluent-boards'),
570 - ], 201);
571 - } catch (\Exception $e) {
572 - 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');
573 721 }
722 +
723 + return $this->sendSuccess([
724 + 'message' => $message,
725 + ], 201);
574 726 }
575 727
576 728
577 729 public function getAddonsSettings()
578 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 +
579 741 $addOns = [
580 742 'fluent-crm' => [
581 743 'title' => __('FluentCRM', 'fluent-boards'),
582 744 'logo' => fluent_boards_mix('images/addons/fluent-crm.svg'),
@@ -583,9 +745,10 @@
583 745 'is_installed' => defined('FLUENTCRM'),
584 746 'learn_more_url' => 'https://fluentcrm.com/',
585 747 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluentcrm/',
586 748 'action_text' => $this->isPluginInstalled('fluent-crm/fluent-crm.php') ? __('Activate FluentCRM', 'fluent-boards') : __('Install FluentCRM', 'fluent-boards'),
587 - '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')
588 751 ],
589 752 'fluentform' => [
590 753 'title' => __('Fluent Forms', 'fluent-boards'),
591 754 'logo' => fluent_boards_mix('images/addons/fluentform.png'),
@@ -592,9 +755,10 @@
592 755 'is_installed' => defined('FLUENTFORM'),
593 756 'learn_more_url' => 'https://wordpress.org/plugins/fluentform/',
594 757 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluent-forms/',
595 758 'action_text' => $this->isPluginInstalled('fluent-form/fluent-form.php') ? __('Activate Fluent Forms', 'fluent-boards') : __('Install Fluent Forms', 'fluent-boards'),
596 - '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')
597 761 ],
598 762 'fluent-support' => [
599 763 'title' => __('Fluent Support', 'fluent-boards'),
600 764 'logo' => fluent_boards_mix('images/addons/fluent-support.svg'),
@@ -602,9 +766,10 @@
602 766 'learn_more_url' => 'https://wordpress.org/plugins/fluent-connect/',
603 767 'settings_url' => admin_url('admin.php?page=fluent-support#/'),
604 768 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluentsupport/',
605 769 'action_text' => $this->isPluginInstalled('fluent-support/fluent-support.php') ? __('Activate Fluent Support', 'fluent-boards') : __('Install Fluent Support', 'fluent-boards'),
606 - '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')
607 772 ],
608 773 'fluent-smtp' => [
609 774 'title' => __('Fluent SMTP', 'fluent-boards'),
610 775 'logo' => fluent_boards_mix('images/addons/fluent-smtp.svg'),
@@ -611,10 +776,24 @@
611 776 'is_installed' => defined('FLUENTMAIL'),
612 777 'learn_more_url' => 'https://wordpress.org/plugins/fluent-smtp/',
613 778 'associate_doc' => admin_url('options-general.php?page=fluent-mail#/'),
614 779 'action_text' => $this->isPluginInstalled('fluent-smtp/fluent-smtp.php') ? __('Activate Fluent SMTP', 'fluent-boards') : __('Install Fluent SMTP', 'fluent-boards'),
615 - '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')
616 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 + ],
617 796 ];
618 797
619 798 $addOns = apply_filters('fluent_boards/addons_settings', $addOns);
620 799
@@ -639,16 +818,27 @@
639 818 'message' => __('This feature is only available in Fluent Boards Pro', 'fluent-boards')
640 819 ]);
641 820 }
642 821
643 - $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 + }
644 830
645 831 $prefSettings = fluent_boards_get_pref_settings(false);
646 832
647 - $settings = wp_parse_args($settings, $prefSettings);
833 + $settings = wp_parse_args($rawSettings, $prefSettings);
648 834
649 835 $settings = Arr::only($settings, array_keys($prefSettings));
650 - $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 + }
651 841
652 842 if (empty($settings['frontend']['slug'])) {
653 843 $settings['frontend']['slug'] = 'projects';
654 844 }
@@ -660,8 +850,12 @@
660 850 do_action('fluent_boards/saving_addons', $settings, $prefSettings);
661 851
662 852 update_option('fluent_boards_modules', $settings, 'yes');
663 853
854 + if (isset($settings['recurring_task']['enabled']) && $settings['recurring_task']['enabled'] == 'no') {
855 + do_action('fluent_boards/recurring_task_disabled');
856 + }
857 +
664 858 return $this->sendSuccess([
665 859 'message' => __('Settings are saved', 'fluent-boards'),
666 860 'featureModules' => $settings
667 861 ]);
@@ -670,9 +864,9 @@
670 864 public function installPlugin(Request $request)
671 865 {
672 866 if (!current_user_can('install_plugins')) {
673 867 return $this->sendError([
674 - 'message' => __('Sorry! you do not have permission to install plugin', 'fluent-crm')
868 + 'message' => __('Sorry! you do not have permission to install plugin', 'fluent-boards')
675 869 ]);
676 870 }
677 871
678 872 $plugin = $request->getSafe('plugin', 'sanitize_text_field');
@@ -714,8 +908,19 @@
714 908 {
715 909 return file_exists(WP_PLUGIN_DIR . '/' . $plugin);
716 910 }
717 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 +
718 923 private function backgroundInstaller($plugin_to_install, $plugin_id)
719 924 {
720 925 if (!empty($plugin_to_install['repo-slug'])) {
721 926 require_once ABSPATH . 'wp-admin/includes/file.php';
@@ -767,9 +972,9 @@
767 972 )
768 973 );
769 974
770 975 if (is_wp_error($plugin_information)) {
771 - throw new \Exception($plugin_information->get_error_message());
976 + throw new \Exception(esc_html($plugin_information->get_error_message()));
772 977 }
773 978
774 979 $package = $plugin_information->download_link;
775 980 $download = $upgrader->download_package($package);
@@ -774,15 +979,15 @@
774 979 $package = $plugin_information->download_link;
775 980 $download = $upgrader->download_package($package);
776 981
777 982 if (is_wp_error($download)) {
778 - throw new \Exception($download->get_error_message());
983 + throw new \Exception(esc_html($download->get_error_message()));
779 984 }
780 985
781 986 $working_dir = $upgrader->unpack_package($download, true);
782 987
783 988 if (is_wp_error($working_dir)) {
784 - throw new \Exception($working_dir->get_error_message());
989 + throw new \Exception(esc_html($working_dir->get_error_message()));
785 990 }
786 991
787 992 $result = $upgrader->install_package(
788 993 array(
@@ -798,9 +1003,9 @@
798 1003 )
799 1004 );
800 1005
801 1006 if (is_wp_error($result)) {
802 - throw new \Exception($result->get_error_message());
1007 + throw new \Exception(esc_html($result->get_error_message()));
803 1008 }
804 1009
805 1010 $activate = true;
806 1011
@@ -818,9 +1023,9 @@
818 1023 try {
819 1024 $result = activate_plugin($installed ? $installed_plugins[$plugin_file] : $plugin_slug . '/' . $plugin_file);
820 1025
821 1026 if (is_wp_error($result)) {
822 - throw new \Exception($result->get_error_message());
1027 + throw new \Exception(esc_html($result->get_error_message()));
823 1028 }
824 1029 } catch (\Exception $e) {
825 1030 }
826 1031 }
@@ -861,9 +1066,10 @@
861 1066 $pages = [];
862 1067 foreach ($allPages as $page) {
863 1068 $pages[] = [
864 1069 'id' => $page->ID,
865 - '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))
866 1072 ];
867 1073 }
868 1074
869 1075 return $this->sendSuccess([
@@ -889,9 +1095,25 @@
889 1095 return $this->sendError([
890 1096 'message' => __('This feature is only available in Fluent Boards Pro. Please upgrade.', 'fluent-boards')
891 1097 ]);
892 1098 }
893 - $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 + }
894 1116
895 1117 $settings = apply_filters('fluent_boards/save_general_settings', $settings);
896 1118
897 1119 $savedSettings = fluent_boards_update_option('general_settings', $settings);
@@ -898,9 +1120,11 @@
898 1120 $savedGeneralSettings = \maybe_unserialize($savedSettings->value);
899 1121
900 1122 $scheduleHandler = new ProScheduleHandler();
901 1123
902 - 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)) {
903 1127 // force schedule from this settings update
904 1128 $scheduleHandler->clearDailyTaskReminderScheduler();
905 1129 $scheduleHandler->scheduleDailyTaskReminder();
906 1130 }