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 +394 -97 1.13trunk 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;
@@ -14,8 +15,9 @@
14 15 use FluentBoards\App\Services\Constant;
15 16 use FluentBoards\App\Services\PermissionManager;
16 17 use FluentBoards\Framework\Http\Request\Request;
17 18 use FluentBoards\Framework\Support\Arr;
19 +use FluentBoardsPro\App\Hooks\Handlers\ProScheduleHandler;
18 20
19 21 class OptionsController extends Controller
20 22 {
21 23 private $boardService;
@@ -30,18 +32,18 @@
30 32
31 33 public function selectorOptions(Request $request)
32 34 {
33 35 try {
34 - $optionKey = $request->getSafe('option_key');
35 - $search = $request->getSafe('search');
36 + $optionKey = $request->getSafe('option_key', 'sanitize_text_field');
37 + $search = $request->getSafe('search', 'sanitize_text_field');
36 38 $includedIds = $request->getSafe('values');
37 - $boardId = $request->getSafe('board_id');
39 + $boardId = $request->getSafe('board_id', 'intval');
38 40
39 41 $options = [];
40 42 if ('users' === $optionKey || 'task_assignees' === $optionKey) { // no ajax/code is designed to handle this eventually will goto else
41 43
42 44 if (!PermissionManager::isBoardManager($boardId)) {
43 - 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'));
44 46 }
45 47
46 48 if (!defined('FLUENT_BOARDS_PRO')) {
47 49 // get who has 'manage_options' capability
@@ -47,20 +49,38 @@
47 49 // get who has 'manage_options' capability
48 50 $users = PermissionManager::getAll_WP_Admins($search);
49 51
50 52 } else {
51 - $users = User::query()
52 - ->when($search, function ($query) use ($search) {
53 - return $query->where('display_name', 'LIKE', '%' . $search . '%')->orWhere('user_email', 'LIKE', '%' . $search . '%');
54 - })
55 - ->limit(20)->get();
53 + // Search by user login, email, and nicename, first_name , last_name
54 + $users = Helper::searchWordPressUsers($search);
55 + $users = Helper::sanitizeUsersArray($users, $boardId);
56 +
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,70 +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
449 - $tasksQuery = Task::query()->where('parent_id', null)->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']);
450 - $isUserAdmin = PermissionManager::isAdmin($currentUserId);
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));
451 558
452 - // This is a check for deleted tasks to be excluded from search results
453 - $allActiveBoardsIds = Board::query()->where('archived_at', null)->pluck('id')->toArray();
559 + // Build base queries
560 + $firstThreeChars = substr($query, 0, 3);
561 + $firstNineChars = substr($query, 0, 9);
454 562
563 + if($firstThreeChars == 'id:') {
564 + $idPart = preg_replace('/[^a-zA-Z0-9]/', '', substr($query, 3));
565 + $tasksQuery = Task::query()->where('parent_id', null)->whereRaw('id LIKE ?', ['%' . $idPart . '%']);
566 + $boardQuery = Board::query()->whereRaw('id LIKE ?', ['%' . $idPart . '%']);
567 + }elseif($firstNineChars == 'archived:') {
568 + $archivedPart = trim(substr($query, 9));
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 . '%']);
571 + } else {
572 + $tasksQuery = Task::query()->where('parent_id', null)->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']);
573 + $boardQuery = Board::query()->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']);
574 + }
575 +
576 + // Apply scope and permissions
577 + $isUserAdmin = PermissionManager::isAdmin($currentUserId);
455 578 if ($scope == 'all') {
456 - $boardQuery = Board::query()->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']);
457 - if ($isUserAdmin) {
458 - $boards = $boardQuery->get();
459 - $tasks = $tasksQuery->get();
460 - } else {
579 + if (!$isUserAdmin) {
461 580 $boardIds = PermissionManager::getBoardIdsForUser($currentUserId);
462 - $boards = $boardQuery->whereIn('id', $boardIds)->get();
463 - $tasks = $tasksQuery->whereIn('board_id', $boardIds)->get();
581 + $boardQuery->whereIn('id', $boardIds);
582 + $tasksQuery->whereIn('board_id', $boardIds);
464 583 }
465 584 } else {
466 - $boards = []; // boards results is not needed in scoped search
467 - $inBoard = (int)$scope;
468 - if ($isUserAdmin || in_array($inBoard, $boardIds = PermissionManager::getBoardIdsForUser($currentUserId))) {
469 - $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);
470 591 } else {
471 - // Out of permission scope search
472 - $tasks = [];
592 + $tasksQuery->where('id', -1); // Force no results
473 593 }
474 594 }
475 595
596 + $allActiveBoardsIds = Board::query()->where('archived_at', null)->pluck('id')->toArray();
476 597
598 + $boards = [];
599 + $tasks = [];
600 + $totalBoards = 0;
601 + $totalTasks = 0;
602 + $formattedBoards = [];
477 603 $formattedTasks = [];
478 - $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 +
479 619 foreach ($boards as $board) {
480 620 $formattedBoards[] = [
481 621 'type' => 'board',
482 622 'id' => $board->id,
483 623 'title' => $board->title,
484 - 'description' => $board->description,
485 - 'url' => Helper::getBoardUrl($board->id)
624 + 'description' => DescriptionMarkdownConverter::normalize($board->description),
486 625 ];
487 626 }
488 627 foreach ($tasks as $task) {
489 -
490 628 if (!in_array($task->board_id, $allActiveBoardsIds)) {
491 - // if the task is not in an active board, skip it
492 629 continue;
493 630 }
494 631
495 632 $board = $task->board;
496 -
497 633 $formattedTasks[] = [
498 634 'type' => 'task',
499 635 'id' => $task->id,
500 636 'title' => $task->title,
501 - 'description' => $task->description,
502 - 'url' => Helper::getTaskUrl($task->id, $board->id),
637 + 'description' => DescriptionMarkdownConverter::normalize($task->description),
638 + 'board_id' => $task->board_id,
503 639 'board' => [
504 640 'id' => $board->id,
505 641 'title' => $board->title,
506 642 'url' => Helper::getBoardUrl($board->id)
@@ -508,51 +644,101 @@
508 644 'stage' => [
509 645 'id' => $task->stage_id,
510 646 'title' => $task->stage->title ?? '',
511 647 ],
512 -
513 648 ];
514 649 }
650 +
515 651 return $this->sendSuccess([
516 - 'tasks' => $formattedTasks,
517 - '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 + ]
518 666 ], 200);
519 -
520 667 }
521 668
522 - public function getDashboardViewSettings()
669 + public function getDashboardViewSettings(Request $request)
523 670 {
524 - try {
671 + $view = $request->getSafe('view', 'sanitize_text_field');
672 +
673 + if ($view == 'kanbanview') {
525 674 $globalSettings = $this->optionService->getDashboardViewSettings();
526 - if ($globalSettings->value)
527 - $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 + }
528 683
529 - return $this->sendSuccess([
530 - 'currentSettings' => $currentSettings,
531 - ], 200);
532 - } catch (\Exception $e) {
533 - return $this->sendError($e->getMessage(), 404);
534 - }
684 + if ($globalSettings->value)
685 + $currentSettings = maybe_unserialize($globalSettings->value);
686 +
687 + return $this->sendSuccess([
688 + 'currentSettings' => $currentSettings,
689 + ], 200);
535 690 }
536 691
537 692 public function updateDashboardViewSettings(Request $request)
538 693 {
539 - try {
540 - $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');
541 712
542 - $this->optionService->updateDashboardViewSettings($newSettings);
713 + $this->optionService->updateDashboardViewSettings($newSettings, $view);
543 714
544 - return $this->sendSuccess([
545 - 'message' => __("Dashboard view settings are updated", 'fluent-boards'),
546 - ], 201);
547 - } catch (\Exception $e) {
548 - 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');
549 721 }
722 +
723 + return $this->sendSuccess([
724 + 'message' => $message,
725 + ], 201);
550 726 }
551 727
552 728
553 729 public function getAddonsSettings()
554 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 +
555 741 $addOns = [
556 742 'fluent-crm' => [
557 743 'title' => __('FluentCRM', 'fluent-boards'),
558 744 'logo' => fluent_boards_mix('images/addons/fluent-crm.svg'),
@@ -559,9 +745,10 @@
559 745 'is_installed' => defined('FLUENTCRM'),
560 746 'learn_more_url' => 'https://fluentcrm.com/',
561 747 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluentcrm/',
562 748 'action_text' => $this->isPluginInstalled('fluent-crm/fluent-crm.php') ? __('Activate FluentCRM', 'fluent-boards') : __('Install FluentCRM', 'fluent-boards'),
563 - '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')
564 751 ],
565 752 'fluentform' => [
566 753 'title' => __('Fluent Forms', 'fluent-boards'),
567 754 'logo' => fluent_boards_mix('images/addons/fluentform.png'),
@@ -568,9 +755,10 @@
568 755 'is_installed' => defined('FLUENTFORM'),
569 756 'learn_more_url' => 'https://wordpress.org/plugins/fluentform/',
570 757 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluent-forms/',
571 758 'action_text' => $this->isPluginInstalled('fluent-form/fluent-form.php') ? __('Activate Fluent Forms', 'fluent-boards') : __('Install Fluent Forms', 'fluent-boards'),
572 - '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')
573 761 ],
574 762 'fluent-support' => [
575 763 'title' => __('Fluent Support', 'fluent-boards'),
576 764 'logo' => fluent_boards_mix('images/addons/fluent-support.svg'),
@@ -578,9 +766,10 @@
578 766 'learn_more_url' => 'https://wordpress.org/plugins/fluent-connect/',
579 767 'settings_url' => admin_url('admin.php?page=fluent-support#/'),
580 768 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluentsupport/',
581 769 'action_text' => $this->isPluginInstalled('fluent-support/fluent-support.php') ? __('Activate Fluent Support', 'fluent-boards') : __('Install Fluent Support', 'fluent-boards'),
582 - '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')
583 772 ],
584 773 'fluent-smtp' => [
585 774 'title' => __('Fluent SMTP', 'fluent-boards'),
586 775 'logo' => fluent_boards_mix('images/addons/fluent-smtp.svg'),
@@ -587,12 +776,28 @@
587 776 'is_installed' => defined('FLUENTMAIL'),
588 777 'learn_more_url' => 'https://wordpress.org/plugins/fluent-smtp/',
589 778 'associate_doc' => admin_url('options-general.php?page=fluent-mail#/'),
590 779 'action_text' => $this->isPluginInstalled('fluent-smtp/fluent-smtp.php') ? __('Activate Fluent SMTP', 'fluent-boards') : __('Install Fluent SMTP', 'fluent-boards'),
591 - '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')
592 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 + ],
593 796 ];
594 797
798 + $addOns = apply_filters('fluent_boards/addons_settings', $addOns);
799 +
595 800 $modules = fluent_boards_get_pref_settings(false);
596 801
597 802 if (empty($modules['frontend']['render_type'])) {
598 803 $modules['frontend']['render_type'] = 'standalone';
@@ -613,16 +818,27 @@
613 818 'message' => __('This feature is only available in Fluent Boards Pro', 'fluent-boards')
614 819 ]);
615 820 }
616 821
617 - $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 + }
618 830
619 831 $prefSettings = fluent_boards_get_pref_settings(false);
620 832
621 - $settings = wp_parse_args($settings, $prefSettings);
833 + $settings = wp_parse_args($rawSettings, $prefSettings);
622 834
623 835 $settings = Arr::only($settings, array_keys($prefSettings));
624 - $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 + }
625 841
626 842 if (empty($settings['frontend']['slug'])) {
627 843 $settings['frontend']['slug'] = 'projects';
628 844 }
@@ -634,8 +850,12 @@
634 850 do_action('fluent_boards/saving_addons', $settings, $prefSettings);
635 851
636 852 update_option('fluent_boards_modules', $settings, 'yes');
637 853
854 + if (isset($settings['recurring_task']['enabled']) && $settings['recurring_task']['enabled'] == 'no') {
855 + do_action('fluent_boards/recurring_task_disabled');
856 + }
857 +
638 858 return $this->sendSuccess([
639 859 'message' => __('Settings are saved', 'fluent-boards'),
640 860 'featureModules' => $settings
641 861 ]);
@@ -644,15 +864,15 @@
644 864 public function installPlugin(Request $request)
645 865 {
646 866 if (!current_user_can('install_plugins')) {
647 867 return $this->sendError([
648 - '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')
649 869 ]);
650 870 }
651 871
652 872 $plugin = $request->getSafe('plugin', 'sanitize_text_field');
653 873
654 - $acceptedPlugins = [
874 + $acceptedFreePlugins = [
655 875 'fluent-crm' => 'fluent-crm.php',
656 876 'fluentform' => 'fluentform.php',
657 877 'fluent-support' => 'fluent-support.php',
658 878 'fluent-smtp' => 'fluent-smtp.php'
@@ -657,8 +877,10 @@
657 877 'fluent-support' => 'fluent-support.php',
658 878 'fluent-smtp' => 'fluent-smtp.php'
659 879 ];
660 880
881 + $acceptedPlugins = apply_filters('fluent_boards/accepted_plugins', $acceptedFreePlugins);
882 +
661 883 if (!isset($acceptedPlugins[$plugin])) {
662 884 return $this->sendError([
663 885 'message' => __('Invalid plugin', 'fluent-boards')
664 886 ]);
@@ -663,9 +885,8 @@
663 885 'message' => __('Invalid plugin', 'fluent-boards')
664 886 ]);
665 887 }
666 888
667 -
668 889 $pluginToInstall = [
669 890 'name' => __('Fluent Plugin', 'fluent-boards'),
670 891 'repo-slug' => $plugin,
671 892 'file' => $acceptedPlugins[$plugin],
@@ -670,9 +891,14 @@
670 891 'repo-slug' => $plugin,
671 892 'file' => $acceptedPlugins[$plugin],
672 893 ];
673 894
674 - $this->backgroundInstaller($pluginToInstall, $plugin);
895 + // if plugin in free list then run background intaller otherwise call an action to install
896 + if (isset($acceptedFreePlugins[$plugin])) {
897 + $this->backgroundInstaller($pluginToInstall, $plugin);
898 + } else {
899 + do_action('fluent_boards/install_plugin', $pluginToInstall, $plugin);
900 + }
675 901
676 902 return $this->sendSuccess([
677 903 'message' => __('Plugin is being installed', 'fluent-boards')
678 904 ]);
@@ -682,8 +908,19 @@
682 908 {
683 909 return file_exists(WP_PLUGIN_DIR . '/' . $plugin);
684 910 }
685 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 +
686 923 private function backgroundInstaller($plugin_to_install, $plugin_id)
687 924 {
688 925 if (!empty($plugin_to_install['repo-slug'])) {
689 926 require_once ABSPATH . 'wp-admin/includes/file.php';
@@ -735,9 +972,9 @@
735 972 )
736 973 );
737 974
738 975 if (is_wp_error($plugin_information)) {
739 - throw new \Exception($plugin_information->get_error_message());
976 + throw new \Exception(esc_html($plugin_information->get_error_message()));
740 977 }
741 978
742 979 $package = $plugin_information->download_link;
743 980 $download = $upgrader->download_package($package);
@@ -742,15 +979,15 @@
742 979 $package = $plugin_information->download_link;
743 980 $download = $upgrader->download_package($package);
744 981
745 982 if (is_wp_error($download)) {
746 - throw new \Exception($download->get_error_message());
983 + throw new \Exception(esc_html($download->get_error_message()));
747 984 }
748 985
749 986 $working_dir = $upgrader->unpack_package($download, true);
750 987
751 988 if (is_wp_error($working_dir)) {
752 - throw new \Exception($working_dir->get_error_message());
989 + throw new \Exception(esc_html($working_dir->get_error_message()));
753 990 }
754 991
755 992 $result = $upgrader->install_package(
756 993 array(
@@ -766,9 +1003,9 @@
766 1003 )
767 1004 );
768 1005
769 1006 if (is_wp_error($result)) {
770 - throw new \Exception($result->get_error_message());
1007 + throw new \Exception(esc_html($result->get_error_message()));
771 1008 }
772 1009
773 1010 $activate = true;
774 1011
@@ -786,9 +1023,9 @@
786 1023 try {
787 1024 $result = activate_plugin($installed ? $installed_plugins[$plugin_file] : $plugin_slug . '/' . $plugin_file);
788 1025
789 1026 if (is_wp_error($result)) {
790 - throw new \Exception($result->get_error_message());
1027 + throw new \Exception(esc_html($result->get_error_message()));
791 1028 }
792 1029 } catch (\Exception $e) {
793 1030 }
794 1031 }
@@ -829,9 +1066,10 @@
829 1066 $pages = [];
830 1067 foreach ($allPages as $page) {
831 1068 $pages[] = [
832 1069 'id' => $page->ID,
833 - '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))
834 1072 ];
835 1073 }
836 1074
837 1075 return $this->sendSuccess([
@@ -836,7 +1074,66 @@
836 1074
837 1075 return $this->sendSuccess([
838 1076 'pages' => $pages
839 1077 ]);
1078 + }
1079 +
1080 + public function getGeneralSettings()
1081 + {
1082 + $settings = fluent_boards_get_option('general_settings', []);
1083 +
1084 + return $this->sendSuccess([
1085 + 'settings' => $settings,
1086 + 'server_timezone' => \wp_timezone_string()
1087 + ]);
1088 +
1089 + }
1090 +
1091 + public function saveGeneralSettings(Request $request)
1092 + {
1093 + // check for pro version
1094 + if (!defined('FLUENT_BOARDS_PRO')) {
1095 + return $this->sendError([
1096 + 'message' => __('This feature is only available in Fluent Boards Pro. Please upgrade.', 'fluent-boards')
1097 + ]);
1098 + }
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 + }
1116 +
1117 + $settings = apply_filters('fluent_boards/save_general_settings', $settings);
1118 +
1119 + $savedSettings = fluent_boards_update_option('general_settings', $settings);
1120 + $savedGeneralSettings = \maybe_unserialize($savedSettings->value);
1121 +
1122 + $scheduleHandler = new ProScheduleHandler();
1123 +
1124 + $dailyReminderEnabled = $savedGeneralSettings['daily_reminder_enabled'] ?? false;
1125 +
1126 + if (filter_var($dailyReminderEnabled, FILTER_VALIDATE_BOOLEAN)) {
1127 + // force schedule from this settings update
1128 + $scheduleHandler->clearDailyTaskReminderScheduler();
1129 + $scheduleHandler->scheduleDailyTaskReminder();
1130 + }
1131 +
1132 + return $this->sendSuccess([
1133 + 'settings' => $savedGeneralSettings,
1134 + 'message' => __('Settings are saved', 'fluent-boards')
1135 + ]);
1136 +
840 1137 }
841 1138
842 1139 }