PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.2.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.2.0
2.2.0 2.1.0 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 All 43 releases
← All changes | app/Http/Controllers/OptionsController.php +350 -102 1.22 → 2.2.0 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
@@ -55,12 +56,46 @@
55 56
56 57 }
57 58
58 59 $options = $this->addUserDataAsSelectorOption($users);
60 + if ('users' === $optionKey && $options) {
61 + // Resolve plugin admins once for this result set instead of querying per user.
62 + $adminIds = Meta::where('object_type', Constant::FLUENT_BOARD_ADMIN)
63 + ->whereIn('object_id', array_column($options, 'id'))
64 + ->pluck('object_id')
65 + ->toArray();
66 + $adminLookup = array_fill_keys($adminIds, true);
59 67
68 + foreach ($options as &$option) {
69 + $option['is_admin'] = isset($adminLookup[$option['id']])
70 + || PermissionManager::isWPAdmin($option['id']);
71 + }
72 + unset($option);
73 + }
74 +
75 +
76 + } elseif ('board_create_users' === $optionKey) {
77 + if (!PermissionManager::userHasBoardCreationPermission()) {
78 + throw new \Exception(esc_html__('You do not have permission to access this route', 'fluent-boards'));
79 + }
80 +
81 + if (!defined('FLUENT_BOARDS_PRO')) {
82 + // Bound the lookup: this popover searches on focus (empty query too).
83 + $users = PermissionManager::getAll_WP_Admins($search, 20);
84 + } else {
85 + $users = Helper::searchWordPressUsers($search);
86 + }
87 +
88 + $options = $this->addUserDataAsSelectorOption($users);
89 + // Board creation can be delegated to members without `list_users`;
90 + // don't leak full account emails to them.
91 + $options = $this->maskSelectorEmailsForViewer($options);
92 +
60 93 } elseif ('boards' === $optionKey) {
61 94 $boards = Board::query()
95 + ->byAccessUser(get_current_user_id())
62 96 ->when($search, function ($query) use ($search) {
97 + // $search is already sanitized with sanitize_text_field above
63 98 return $query->where('title', 'LIKE', '%' . $search . '%');
64 99 })->take(20)->get();
65 100
66 101 foreach ($boards as $board) {
@@ -71,12 +106,18 @@
71 106 'right_side_value' => $board->slug,
72 107 ];
73 108 }
74 109 } elseif ('tasks' === $optionKey) {
110 + if (!PermissionManager::userHasPermission($boardId)) {
111 + throw new \Exception(esc_html__('You do not have permission to access this route', 'fluent-boards'));
112 + }
113 +
114 + // $boardId is already sanitized with intval above
75 115 $tasks = Task::where('board_id', $boardId)
76 116 ->whereNull('archived_at')
77 117 ->whereNull('parent_id')
78 118 ->when($search, function ($query) use ($search) {
119 + // $search is already sanitized with sanitize_text_field above
79 120 return $query->where('title', 'LIKE', '%' . $search . '%');
80 121 })->take(20)->get();
81 122
82 123 foreach ($tasks as $task) {
@@ -82,15 +123,20 @@
82 123 foreach ($tasks as $task) {
83 124 $options[] = [
84 125 'id' => $task->id,
85 126 'title' => $task->title,
86 - 'board_id' => $task->board_id
127 + 'board_id' => $task->board_id,
128 + 'subtask_groups' => $task->subtaskGroup
87 129 ];
88 130 }
89 131
90 132 } elseif ('assigned_in_task' == $optionKey) {
133 + if (!PermissionManager::userHasPermission($boardId)) {
134 + throw new \Exception(esc_html__('You do not have permission to access this route', 'fluent-boards'));
135 + }
136 +
91 137 $users = (new BoardService())->getAssigneesByBoard($boardId, $search);
92 - $options = $this->addUserDataAsSelectorOption($users);
138 + $options = $this->maskSelectorEmailsForViewer($this->addUserDataAsSelectorOption($users));
93 139 } else {
94 140 $options = apply_filters('fluent_boards/ajax_options_' . $optionKey, [], $search, $includedIds);
95 141 }
96 142
@@ -116,8 +162,42 @@
116 162 }
117 163 return $options;
118 164 }
119 165
166 + /**
167 + * Obfuscate emails in selector options for viewers who lack the `list_users`
168 + * capability, keeping the current user's own email visible.
169 + */
170 + private function maskSelectorEmailsForViewer($options)
171 + {
172 + if (current_user_can('list_users')) {
173 + return $options;
174 + }
175 +
176 + $currentUser = wp_get_current_user();
177 + $currentUserEmail = ($currentUser && isset($currentUser->user_email)) ? $currentUser->user_email : '';
178 +
179 + foreach ($options as $index => $option) {
180 + $email = $option['email'] ?? '';
181 +
182 + if ($email === '' || $email === $currentUserEmail) {
183 + continue;
184 + }
185 +
186 + $maskedEmail = Helper::obfuscateEmail($email);
187 +
188 + // When there's no display name the raw email doubles as the name.
189 + if (($option['name'] ?? '') === $email) {
190 + $options[$index]['name'] = $maskedEmail;
191 + }
192 +
193 + $options[$index]['email'] = $maskedEmail;
194 + $options[$index]['title'] = ($options[$index]['name'] ?? $maskedEmail) . ' (' . $maskedEmail . ')';
195 + }
196 +
197 + return $options;
198 + }
199 +
120 200 public function getCurrentUserPermissions()
121 201 {
122 202 try {
123 203 $currentUserBoards = Relation::query()
@@ -139,10 +219,10 @@
139 219
140 220 public function getUserPermission(Request $request)
141 221 {
142 222 try {
143 - $boardId = $request->getSafe('boardId');
144 - $userId = $request->getSafe('userId');
223 + $boardId = $request->getSafe('boardId', 'intval');
224 + $userId = $request->getSafe('userId', 'intval');
145 225
146 226 $boardUser = Relation::where('board_id', $boardId)
147 227 ->where('user_id', $userId)
148 228 ->where('status', 'ACTIVE')->first();
@@ -160,12 +240,12 @@
160 240
161 241 public function updatedUserPermission(Request $request)
162 242 {
163 243 try {
164 - $permission = $request->getSafe('userPermission');
165 - $updateType = $request->getSafe('updateType');
166 - $boardId = $request->getSafe('boardId');
167 - $userId = $request->getSafe('userId');
244 + $permission = $request->getSafe('userPermission', 'sanitize_text_field');
245 + $updateType = $request->getSafe('updateType', 'sanitize_text_field');
246 + $boardId = $request->getSafe('boardId', 'intval');
247 + $userId = $request->getSafe('userId', 'intval');
168 248
169 249 $boardUser = Relation::where('board_id', $boardId)->where('user_id', $userId)->status('ACTIVE')->first();
170 250
171 251 if ('Board Admin' == $permission) {
@@ -197,8 +277,9 @@
197 277 }
198 278
199 279 public function SetUserSuperAdmin($userId)
200 280 {
281 + $userId = absint($userId);
201 282 try {
202 283 $this->optionService->createSuperAdmin($userId);
203 284 return $this->sendSuccess([
204 285 'message' => __('Member has been set super admin successfully!', 'fluent-boards')
@@ -210,8 +291,9 @@
210 291 }
211 292
212 293 public function removeUserSuperAdmin($userId)
213 294 {
295 + $userId = absint($userId);
214 296 try {
215 297 $this->optionService->removeUserSuperAdmin($userId);
216 298
217 299 return $this->sendSuccess([
@@ -224,9 +306,9 @@
224 306
225 307 public function IsUserAllBoardAdmin(Request $request)
226 308 {
227 309 try {
228 - $userId = $request->getSafe('id');
310 + $userId = $request->getSafe('id', 'intval');
229 311 $isSuperAdmin = false;
230 312 $superAdmin = Relation::where('board_id', null)->where('user_id', $userId)->where('status', 'ACTIVE')->first();
231 313 $totalSuperAdmin = Relation::where('board_id', null)->where('status', 'ACTIVE')->count();
232 314 $permissions = [];
@@ -246,10 +328,11 @@
246 328 }
247 329
248 330 public function RemoveUserFromSuperAdmin(Request $request, $id)
249 331 {
332 + $id = absint($id);
250 333 try {
251 - $userId = $request->getSafe('id');
334 + $userId = $request->getSafe('id', 'intval');
252 335
253 336 $superAdmin = Relation::where('board_id', null)->where('user_id', $userId)->first();
254 337 $superAdmin->status = 'INACTIVE';
255 338 $superAdmin->save();
@@ -264,10 +347,10 @@
264 347
265 348 public function removeUserFromBoard(Request $request)
266 349 {
267 350 try {
268 - $boardId = $request->getSafe('boardId');
269 - $userId = $request->getSafe('userId');
351 + $boardId = $request->getSafe('boardId', 'intval');
352 + $userId = $request->getSafe('userId', 'intval');
270 353
271 354 $this->boardService->removeUserFromBoard($boardId, $userId);
272 355
273 356 if (!PermissionManager::isAdmin($userId)) {
@@ -284,9 +367,14 @@
284 367
285 368 public function addAsSuperAdmin(Request $request)
286 369 {
287 370 try {
288 - $userIds = $request->getSafe('memberIds');
371 + $rawUserIds = $request->getSafe('memberIds');
372 + // Sanitize array of user IDs
373 + $userIds = [];
374 + if (is_array($rawUserIds)) {
375 + $userIds = array_filter(array_map('intval', $rawUserIds));
376 + }
289 377 foreach ($userIds as $userId) {
290 378 $this->createSuperAdmin($userId);
291 379 }
292 380
@@ -315,10 +403,20 @@
315 403
316 404 public function addMembersInBoards(Request $request)
317 405 {
318 406 try {
319 - $userIds = $request->getSafe('memberIds');
320 - $boardIds = $request->getSafe('boardIds');
407 + $rawUserIds = $request->getSafe('memberIds');
408 + $rawBoardIds = $request->getSafe('boardIds');
409 +
410 + // Sanitize arrays of IDs
411 + $userIds = [];
412 + if (is_array($rawUserIds)) {
413 + $userIds = array_filter(array_map('intval', $rawUserIds));
414 + }
415 + $boardIds = [];
416 + if (is_array($rawBoardIds)) {
417 + $boardIds = array_filter(array_map('intval', $rawBoardIds));
418 + }
321 419
322 420 foreach ($userIds as $userId) {
323 421 foreach ($boardIds as $boardId) {
324 422 $this->boardService->addMembersInBoard($boardId, $userId);
@@ -336,9 +434,18 @@
336 434
337 435 public function updateGlobalNotificationSettings(Request $request)
338 436 {
339 437 try {
340 - $newSettings = $request->getSafe('updatedSettings');
438 + // updatedSettings is an array, sanitize each element
439 + $rawSettings = $request->get('updatedSettings');
440 + $newSettings = [];
441 + if (is_array($rawSettings)) {
442 + foreach ($rawSettings as $key => $value) {
443 + $sanitizedKey = sanitize_text_field($key);
444 + $sanitizedValue = sanitize_text_field($value);
445 + $newSettings[$sanitizedKey] = $sanitizedValue;
446 + }
447 + }
341 448
342 449 $this->optionService->updateGlobalNotificationSettings($newSettings);
343 450
344 451 return $this->sendSuccess([
@@ -373,21 +480,29 @@
373 480 }
374 481
375 482 $boardId = $request->getSafe('boardId', 'intval');
376 483
377 - $memberUserIds = Relation::where('object_type', 'board_user')
484 + if ($boardId && !PermissionManager::userHasPermission($boardId)) {
485 + return $this->sendError([
486 + 'message' => __('You do not have permission to access this route', 'fluent-boards')
487 + ], 403);
488 + }
489 +
490 + $memberUserIdsQuery = Relation::where('object_type', Constant::OBJECT_TYPE_BOARD_USER)
378 491 ->select(['foreign_id'])
379 492 ->groupBy('foreign_id');
380 493
381 494 if ($boardId) {
382 - $memberUserIds = $memberUserIds->where('object_id', $boardId);
495 + $memberUserIdsQuery->where('object_id', $boardId);
496 + } elseif (!PermissionManager::isAdmin()) {
497 + $boardIds = array_filter(array_map('intval', PermissionManager::getBoardIdsForUser()));
498 + $memberUserIdsQuery->whereIn('object_id', $boardIds);
383 499 }
384 500
385 - $members = [];
386 -
387 - $memberUserIds = $memberUserIds->get()
501 + $memberUserIds = $memberUserIdsQuery->get()
388 502 ->pluck('foreign_id')->toArray();
389 503
504 + $members = [];
390 505
391 506 if ($memberUserIds) {
392 507 $memberUsers = get_users([
393 508 'include' => $memberUserIds
@@ -437,85 +552,106 @@
437 552 'members' => $members
438 553 ];
439 554 }
440 555
441 - public function quickSearch()
556 + public function globalSearch()
442 557 {
443 558 $currentUserId = get_current_user_id();
444 559
445 - $query = sanitize_text_field($_REQUEST['query']);
446 - $query = strtolower($query);
447 - $scope = sanitize_text_field($_REQUEST['scope']);
560 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
561 + $query = isset($_REQUEST['query']) ? strtolower(sanitize_text_field(wp_unslash($_REQUEST['query']))) : '';
562 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
563 + $scope = isset($_REQUEST['scope']) ? sanitize_text_field(wp_unslash($_REQUEST['scope'])) : 'all';
448 564
565 + // Pagination parameters
566 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
567 + $taskPage = isset($_REQUEST['task_page']) ? max(1, (int)$_REQUEST['task_page']) : 0;
568 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
569 + $boardPage = isset($_REQUEST['board_page']) ? max(1, (int)$_REQUEST['board_page']) : 0;
570 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
571 + $perPage = isset($_REQUEST['per_page']) ? (int)$_REQUEST['per_page'] : 20;
572 + $perPage = max(1, min(100, $perPage));
573 +
574 + // Build base queries
449 575 $firstThreeChars = substr($query, 0, 3);
576 + $firstNineChars = substr($query, 0, 9);
450 577
451 578 if($firstThreeChars == 'id:') {
452 - $idPart = substr($query, 3);
453 - $idPart = preg_replace('/[^a-zA-Z0-9]/', '', $idPart);
579 + $idPart = preg_replace('/[^a-zA-Z0-9]/', '', substr($query, 3));
454 580 $tasksQuery = Task::query()->where('parent_id', null)->whereRaw('id LIKE ?', ['%' . $idPart . '%']);
581 + $boardQuery = Board::query()->whereRaw('id LIKE ?', ['%' . $idPart . '%']);
582 + }elseif($firstNineChars == 'archived:') {
583 + $archivedPart = trim(substr($query, 9));
584 + $tasksQuery = Task::query()->where('parent_id', null)->whereNotNull('archived_at')->whereRaw('LOWER(title) LIKE ?', ['%' . $archivedPart . '%']);
585 + $boardQuery = Board::query()->whereNotNull('archived_at')->whereRaw('LOWER(title) LIKE ?', ['%' . $archivedPart . '%']);
455 586 } else {
456 587 $tasksQuery = Task::query()->where('parent_id', null)->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']);
588 + $boardQuery = Board::query()->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']);
457 589 }
458 590
591 + // Apply scope and permissions
459 592 $isUserAdmin = PermissionManager::isAdmin($currentUserId);
460 -
461 - // This is a check for deleted tasks to be excluded from search results
462 - $allActiveBoardsIds = Board::query()->where('archived_at', null)->pluck('id')->toArray();
463 -
464 593 if ($scope == 'all') {
465 - if($firstThreeChars == 'id:'){
466 - $idPart = substr($query, 3);
467 - $idPart = preg_replace('/[^a-zA-Z0-9]/', '', $idPart);
468 - $boardQuery = Board::query()->whereRaw('id LIKE ?', ['%' . $idPart . '%']);
469 - } else {
470 - $boardQuery = Board::query()->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']);
471 - }
472 - if ($isUserAdmin) {
473 - $boards = $boardQuery->get();
474 - $tasks = $tasksQuery->get();
475 - } else {
594 + if (!$isUserAdmin) {
476 595 $boardIds = PermissionManager::getBoardIdsForUser($currentUserId);
477 - $boards = $boardQuery->whereIn('id', $boardIds)->get();
478 - $tasks = $tasksQuery->whereIn('board_id', $boardIds)->get();
596 + $boardQuery->whereIn('id', $boardIds);
597 + $tasksQuery->whereIn('board_id', $boardIds);
479 598 }
480 599 } else {
481 - $boards = []; // boards results is not needed in scoped search
482 - $inBoard = (int)$scope;
483 - if ($isUserAdmin || in_array($inBoard, $boardIds = PermissionManager::getBoardIdsForUser($currentUserId))) {
484 - $tasks = $tasksQuery->where('board_id', $inBoard)->get();
600 + // For 'current_board' scope, we don't search boards
601 + $boardQuery->where('id', -1);
602 +
603 + $inBoard = absint($scope);
604 + if ($isUserAdmin || in_array($inBoard, PermissionManager::getBoardIdsForUser($currentUserId))) {
605 + $tasksQuery->where('board_id', $inBoard);
485 606 } else {
486 - // Out of permission scope search
487 - $tasks = [];
607 + $tasksQuery->where('id', -1); // Force no results
488 608 }
489 609 }
490 610
611 + $allActiveBoardsIds = Board::query()->where('archived_at', null)->pluck('id')->toArray();
491 612
613 + $boards = [];
614 + $tasks = [];
615 + $totalBoards = 0;
616 + $totalTasks = 0;
617 + $formattedBoards = [];
492 618 $formattedTasks = [];
493 - $formattedBoards = [];
619 +
620 + // Fetch Boards if requested
621 + if ($boardPage > 0) {
622 + $totalBoards = $boardQuery->count();
623 + $boardOffset = ($boardPage - 1) * $perPage;
624 + $boards = $boardQuery->skip($boardOffset)->take($perPage)->get();
625 + }
626 +
627 + // Fetch Tasks if requested
628 + if ($taskPage > 0) {
629 + $totalTasks = $tasksQuery->count();
630 + $taskOffset = ($taskPage - 1) * $perPage;
631 + $tasks = $tasksQuery->skip($taskOffset)->take($perPage)->get();
632 + }
633 +
494 634 foreach ($boards as $board) {
495 635 $formattedBoards[] = [
496 636 'type' => 'board',
497 637 'id' => $board->id,
498 638 'title' => $board->title,
499 - 'description' => $board->description,
500 - 'url' => Helper::getBoardUrl($board->id)
639 + 'description' => DescriptionMarkdownConverter::normalize($board->description),
501 640 ];
502 641 }
503 642 foreach ($tasks as $task) {
504 -
505 643 if (!in_array($task->board_id, $allActiveBoardsIds)) {
506 - // if the task is not in an active board, skip it
507 644 continue;
508 645 }
509 646
510 647 $board = $task->board;
511 -
512 648 $formattedTasks[] = [
513 649 'type' => 'task',
514 650 'id' => $task->id,
515 651 'title' => $task->title,
516 - 'description' => $task->description,
517 - 'url' => Helper::getTaskUrl($task->id, $board->id),
652 + 'description' => DescriptionMarkdownConverter::normalize($task->description),
653 + 'board_id' => $task->board_id,
518 654 'board' => [
519 655 'id' => $board->id,
520 656 'title' => $board->title,
521 657 'url' => Helper::getBoardUrl($board->id)
@@ -523,51 +659,101 @@
523 659 'stage' => [
524 660 'id' => $task->stage_id,
525 661 'title' => $task->stage->title ?? '',
526 662 ],
527 -
528 663 ];
529 664 }
665 +
530 666 return $this->sendSuccess([
531 - 'tasks' => $formattedTasks,
532 - 'boards' => $formattedBoards
667 + 'tasks' => [
668 + 'data' => $formattedTasks,
669 + 'current_page' => $taskPage,
670 + 'per_page' => $perPage,
671 + 'total' => $totalTasks,
672 + 'last_page' => (int) ceil($totalTasks / $perPage)
673 + ],
674 + 'boards' => [
675 + 'data' => $formattedBoards,
676 + 'current_page' => $boardPage,
677 + 'per_page' => $perPage,
678 + 'total' => $totalBoards,
679 + 'last_page' => (int) ceil($totalBoards / $perPage)
680 + ]
533 681 ], 200);
534 -
535 682 }
536 683
537 - public function getDashboardViewSettings()
684 + public function getDashboardViewSettings(Request $request)
538 685 {
539 - try {
686 + $view = $request->getSafe('view', 'sanitize_text_field');
687 +
688 + if ($view == 'kanbanview') {
540 689 $globalSettings = $this->optionService->getDashboardViewSettings();
541 - if ($globalSettings->value)
542 - $currentSettings = maybe_unserialize($globalSettings->value);
690 + } elseif ($view == 'listview') {
691 + $globalSettings = $this->optionService->getListViewPreferences();
692 + } elseif ($view == 'tableview') {
693 + $globalSettings = $this->optionService->getTableViewPreferences();
694 + } else {
695 + // Handle invalid view or default to one
696 + return $this->sendError(['message' => __('Invalid view type', 'fluent-boards')], 400);
697 + }
543 698
544 - return $this->sendSuccess([
545 - 'currentSettings' => $currentSettings,
546 - ], 200);
547 - } catch (\Exception $e) {
548 - return $this->sendError($e->getMessage(), 404);
549 - }
699 + if ($globalSettings->value)
700 + $currentSettings = maybe_unserialize($globalSettings->value);
701 +
702 + return $this->sendSuccess([
703 + 'currentSettings' => $currentSettings,
704 + ], 200);
550 705 }
551 706
552 707 public function updateDashboardViewSettings(Request $request)
553 708 {
554 - try {
555 - $newSettings = $request->getSafe('updatedSettings');
709 + // updatedSettings is an array, sanitize each element
710 + $rawSettings = $request->get('updatedSettings');
711 + $newSettings = [];
712 + if (is_array($rawSettings)) {
713 + foreach ($rawSettings as $key => $value) {
714 + $sanitizedKey = sanitize_text_field($key);
715 + // Value could be string, boolean, or number - sanitize appropriately
716 + if (is_string($value)) {
717 + $sanitizedValue = sanitize_text_field($value);
718 + } elseif (is_bool($value) || is_numeric($value)) {
719 + $sanitizedValue = $value;
720 + } else {
721 + $sanitizedValue = sanitize_text_field((string)$value);
722 + }
723 + $newSettings[$sanitizedKey] = $sanitizedValue;
724 + }
725 + }
726 + $view = $request->getSafe('view', 'sanitize_text_field');
556 727
557 - $this->optionService->updateDashboardViewSettings($newSettings);
728 + $this->optionService->updateDashboardViewSettings($newSettings, $view);
558 729
559 - return $this->sendSuccess([
560 - 'message' => __("Dashboard view settings are updated", 'fluent-boards'),
561 - ], 201);
562 - } catch (\Exception $e) {
563 - return $this->sendError($e->getMessage(), 404);
730 + if ($view == 'listview') {
731 + $message = __("List view settings updated successfully", 'fluent-boards');
732 + } elseif ($view == 'tableview') {
733 + $message = __("Table view settings updated successfully", 'fluent-boards');
734 + } else {
735 + $message = __("Card view settings updated successfully", 'fluent-boards');
564 736 }
737 +
738 + return $this->sendSuccess([
739 + 'message' => $message,
740 + ], 201);
565 741 }
566 742
567 743
568 744 public function getAddonsSettings()
569 745 {
746 + $canAutoInstallKit = $this->canAutoInstallFluentKit();
747 + $kitPluginFile = 'fluent-toolkit/fluent-toolkit.php';
748 + $kitLoaded = defined('FLUENT_TOOLKIT_VERSION');
749 + $kitPluginExists = $this->isPluginInstalled($kitPluginFile);
750 + $kitActionText = __('Get FluentHub from GitHub', 'fluent-boards');
751 +
752 + if ($canAutoInstallKit) {
753 + $kitActionText = $kitPluginExists ? __('Activate FluentHub', 'fluent-boards') : __('Install FluentHub', 'fluent-boards');
754 + }
755 +
570 756 $addOns = [
571 757 'fluent-crm' => [
572 758 'title' => __('FluentCRM', 'fluent-boards'),
573 759 'logo' => fluent_boards_mix('images/addons/fluent-crm.svg'),
@@ -574,9 +760,10 @@
574 760 'is_installed' => defined('FLUENTCRM'),
575 761 'learn_more_url' => 'https://fluentcrm.com/',
576 762 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluentcrm/',
577 763 'action_text' => $this->isPluginInstalled('fluent-crm/fluent-crm.php') ? __('Activate FluentCRM', 'fluent-boards') : __('Install FluentCRM', 'fluent-boards'),
578 - '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')
764 + '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'),
765 + 'short_desc' => __('Email marketing automation', 'fluent-boards')
579 766 ],
580 767 'fluentform' => [
581 768 'title' => __('Fluent Forms', 'fluent-boards'),
582 769 'logo' => fluent_boards_mix('images/addons/fluentform.png'),
@@ -583,9 +770,10 @@
583 770 'is_installed' => defined('FLUENTFORM'),
584 771 'learn_more_url' => 'https://wordpress.org/plugins/fluentform/',
585 772 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluent-forms/',
586 773 'action_text' => $this->isPluginInstalled('fluent-form/fluent-form.php') ? __('Activate Fluent Forms', 'fluent-boards') : __('Install Fluent Forms', 'fluent-boards'),
587 - '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')
774 + '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'),
775 + 'short_desc' => __('Create forms and accept payments', 'fluent-boards')
588 776 ],
589 777 'fluent-support' => [
590 778 'title' => __('Fluent Support', 'fluent-boards'),
591 779 'logo' => fluent_boards_mix('images/addons/fluent-support.svg'),
@@ -593,9 +781,10 @@
593 781 'learn_more_url' => 'https://wordpress.org/plugins/fluent-connect/',
594 782 'settings_url' => admin_url('admin.php?page=fluent-support#/'),
595 783 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluentsupport/',
596 784 'action_text' => $this->isPluginInstalled('fluent-support/fluent-support.php') ? __('Activate Fluent Support', 'fluent-boards') : __('Install Fluent Support', 'fluent-boards'),
597 - 'description' => __('WordPress Helpdesk and Customer Support Ticket Plugin. Provide awesome support and manage customer queries right from your WordPress dashboard.', 'fluent-boards')
785 + 'description' => __('WordPress Helpdesk and Customer Support Ticket Plugin. Provide awesome support and manage customer queries right from your WordPress dashboard.', 'fluent-boards'),
786 + 'short_desc' => __('Customer support ticketing', 'fluent-boards')
598 787 ],
599 788 'fluent-smtp' => [
600 789 'title' => __('Fluent SMTP', 'fluent-boards'),
601 790 'logo' => fluent_boards_mix('images/addons/fluent-smtp.svg'),
@@ -602,10 +791,24 @@
602 791 'is_installed' => defined('FLUENTMAIL'),
603 792 'learn_more_url' => 'https://wordpress.org/plugins/fluent-smtp/',
604 793 'associate_doc' => admin_url('options-general.php?page=fluent-mail#/'),
605 794 'action_text' => $this->isPluginInstalled('fluent-smtp/fluent-smtp.php') ? __('Activate Fluent SMTP', 'fluent-boards') : __('Install Fluent SMTP', 'fluent-boards'),
606 - 'description' => __('The Ultimate SMTP and SES Plugin for WordPress. Connect with any SMTP, SendGrid, Mailgun, SES, Sendinblue, PepiPost, Google, Microsoft and more.', 'fluent-boards')
795 + 'description' => __('The Ultimate SMTP and SES Plugin for WordPress. Connect with any SMTP, SendGrid, Mailgun, SES, Sendinblue, PepiPost, Google, Microsoft and more.', 'fluent-boards'),
796 + 'short_desc' => __('Reliable email delivery with SMTP', 'fluent-boards')
607 797 ],
798 + 'fluent-toolkit' => [
799 + 'title' => __('FluentHub', 'fluent-boards'),
800 + 'logo' => fluent_boards_mix('images/addons/fluent-toolkit.svg'),
801 + 'is_installed' => $kitLoaded,
802 + 'learn_more_url' => 'https://github.com/WPManageNinja/fluent-toolkit',
803 + 'settings_url' => admin_url('admin.php?page=fluent-toolkit'),
804 + 'associate_doc' => 'https://github.com/WPManageNinja/fluent-toolkit',
805 + 'action_text' => $kitActionText,
806 + 'install_route' => $canAutoInstallKit ? 'admin/mcp/install-adapter' : '',
807 + 'install_url' => $canAutoInstallKit ? '' : 'https://github.com/WPManageNinja/fluent-toolkit',
808 + 'description' => __('Fluent Boards MCP tools become available after FluentHub is installed and active.', 'fluent-boards'),
809 + 'short_desc' => __('AI agent tools for Fluent Boards', 'fluent-boards')
810 + ],
608 811 ];
609 812
610 813 $addOns = apply_filters('fluent_boards/addons_settings', $addOns);
611 814
@@ -630,16 +833,27 @@
630 833 'message' => __('This feature is only available in Fluent Boards Pro', 'fluent-boards')
631 834 ]);
632 835 }
633 836
634 - $settings = $request->get('settings', []);
837 + $rawSettings = $request->get('settings', []);
838 +
839 + // Validate that settings is an array
840 + if (!is_array($rawSettings)) {
841 + return $this->sendError([
842 + 'message' => __('Invalid settings format', 'fluent-boards')
843 + ], 400);
844 + }
635 845
636 846 $prefSettings = fluent_boards_get_pref_settings(false);
637 847
638 - $settings = wp_parse_args($settings, $prefSettings);
848 + $settings = wp_parse_args($rawSettings, $prefSettings);
639 849
640 850 $settings = Arr::only($settings, array_keys($prefSettings));
641 - $settings['frontend']['slug'] = sanitize_title($settings['frontend']['slug']);
851 +
852 + // Sanitize slug if it exists
853 + if (isset($settings['frontend']['slug'])) {
854 + $settings['frontend']['slug'] = sanitize_title($settings['frontend']['slug']);
855 + }
642 856
643 857 if (empty($settings['frontend']['slug'])) {
644 858 $settings['frontend']['slug'] = 'projects';
645 859 }
@@ -651,8 +865,12 @@
651 865 do_action('fluent_boards/saving_addons', $settings, $prefSettings);
652 866
653 867 update_option('fluent_boards_modules', $settings, 'yes');
654 868
869 + if (isset($settings['recurring_task']['enabled']) && $settings['recurring_task']['enabled'] == 'no') {
870 + do_action('fluent_boards/recurring_task_disabled');
871 + }
872 +
655 873 return $this->sendSuccess([
656 874 'message' => __('Settings are saved', 'fluent-boards'),
657 875 'featureModules' => $settings
658 876 ]);
@@ -661,9 +879,9 @@
661 879 public function installPlugin(Request $request)
662 880 {
663 881 if (!current_user_can('install_plugins')) {
664 882 return $this->sendError([
665 - 'message' => __('Sorry! you do not have permission to install plugin', 'fluent-crm')
883 + 'message' => __('Sorry! you do not have permission to install plugin', 'fluent-boards')
666 884 ]);
667 885 }
668 886
669 887 $plugin = $request->getSafe('plugin', 'sanitize_text_field');
@@ -705,8 +923,19 @@
705 923 {
706 924 return file_exists(WP_PLUGIN_DIR . '/' . $plugin);
707 925 }
708 926
927 + private function canAutoInstallFluentKit()
928 + {
929 + $canAutoInstall = (bool) apply_filters('fluent_kit/can_auto_install', false);
930 +
931 + if (!$canAutoInstall) {
932 + $canAutoInstall = (bool) apply_filters('fluent_toolkit/can_auto_install', false);
933 + }
934 +
935 + return $canAutoInstall;
936 + }
937 +
709 938 private function backgroundInstaller($plugin_to_install, $plugin_id)
710 939 {
711 940 if (!empty($plugin_to_install['repo-slug'])) {
712 941 require_once ABSPATH . 'wp-admin/includes/file.php';
@@ -758,9 +987,9 @@
758 987 )
759 988 );
760 989
761 990 if (is_wp_error($plugin_information)) {
762 - throw new \Exception($plugin_information->get_error_message());
991 + throw new \Exception(esc_html($plugin_information->get_error_message()));
763 992 }
764 993
765 994 $package = $plugin_information->download_link;
766 995 $download = $upgrader->download_package($package);
@@ -765,15 +994,15 @@
765 994 $package = $plugin_information->download_link;
766 995 $download = $upgrader->download_package($package);
767 996
768 997 if (is_wp_error($download)) {
769 - throw new \Exception($download->get_error_message());
998 + throw new \Exception(esc_html($download->get_error_message()));
770 999 }
771 1000
772 1001 $working_dir = $upgrader->unpack_package($download, true);
773 1002
774 1003 if (is_wp_error($working_dir)) {
775 - throw new \Exception($working_dir->get_error_message());
1004 + throw new \Exception(esc_html($working_dir->get_error_message()));
776 1005 }
777 1006
778 1007 $result = $upgrader->install_package(
779 1008 array(
@@ -789,9 +1018,9 @@
789 1018 )
790 1019 );
791 1020
792 1021 if (is_wp_error($result)) {
793 - throw new \Exception($result->get_error_message());
1022 + throw new \Exception(esc_html($result->get_error_message()));
794 1023 }
795 1024
796 1025 $activate = true;
797 1026
@@ -809,9 +1038,9 @@
809 1038 try {
810 1039 $result = activate_plugin($installed ? $installed_plugins[$plugin_file] : $plugin_slug . '/' . $plugin_file);
811 1040
812 1041 if (is_wp_error($result)) {
813 - throw new \Exception($result->get_error_message());
1042 + throw new \Exception(esc_html($result->get_error_message()));
814 1043 }
815 1044 } catch (\Exception $e) {
816 1045 }
817 1046 }
@@ -852,9 +1081,10 @@
852 1081 $pages = [];
853 1082 foreach ($allPages as $page) {
854 1083 $pages[] = [
855 1084 'id' => $page->ID,
856 - 'title' => $page->post_title ? $page->post_title : __('(no title)', 'fluent-boards')
1085 + 'title' => $page->post_title ? $page->post_title : __('(no title)', 'fluent-boards'),
1086 + 'url' => esc_url_raw(get_permalink($page->ID))
857 1087 ];
858 1088 }
859 1089
860 1090 return $this->sendSuccess([
@@ -880,9 +1110,25 @@
880 1110 return $this->sendError([
881 1111 'message' => __('This feature is only available in Fluent Boards Pro. Please upgrade.', 'fluent-boards')
882 1112 ]);
883 1113 }
884 - $settings = $request->getSafe('updatedSettings', []);
1114 + // updatedSettings is an array, sanitize each element
1115 + $rawSettings = $request->get('updatedSettings', []);
1116 + $settings = [];
1117 + if (is_array($rawSettings)) {
1118 + foreach ($rawSettings as $key => $value) {
1119 + $sanitizedKey = sanitize_text_field($key);
1120 + // Value could be string, boolean, or number - sanitize appropriately
1121 + if (is_string($value)) {
1122 + $sanitizedValue = sanitize_text_field($value);
1123 + } elseif (is_bool($value) || is_numeric($value)) {
1124 + $sanitizedValue = $value;
1125 + } else {
1126 + $sanitizedValue = sanitize_text_field((string)$value);
1127 + }
1128 + $settings[$sanitizedKey] = $sanitizedValue;
1129 + }
1130 + }
885 1131
886 1132 $settings = apply_filters('fluent_boards/save_general_settings', $settings);
887 1133
888 1134 $savedSettings = fluent_boards_update_option('general_settings', $settings);
@@ -889,12 +1135,14 @@
889 1135 $savedGeneralSettings = \maybe_unserialize($savedSettings->value);
890 1136
891 1137 $scheduleHandler = new ProScheduleHandler();
892 1138
893 - if ($savedGeneralSettings['daily_reminder_enabled'] || $savedGeneralSettings['daily_reminder_enabled'] == 'true') {
1139 + $dailyReminderEnabled = $savedGeneralSettings['daily_reminder_enabled'] ?? false;
1140 +
1141 + if (filter_var($dailyReminderEnabled, FILTER_VALIDATE_BOOLEAN)) {
894 1142 // force schedule from this settings update
895 - $scheduleHandler->clearSchedule();
896 - $scheduleHandler->schedule();
1143 + $scheduleHandler->clearDailyTaskReminderScheduler();
1144 + $scheduleHandler->scheduleDailyTaskReminder();
897 1145 }
898 1146
899 1147 return $this->sendSuccess([
900 1148 'settings' => $savedGeneralSettings,