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.1 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 All 44 releases
← All changes | app/Http/Controllers/OptionsController.php +380 -100 1.20 → 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
@@ -50,17 +51,51 @@
50 51
51 52 } else {
52 53 // Search by user login, email, and nicename, first_name , last_name
53 54 $users = Helper::searchWordPressUsers($search);
54 - $users = Helper::sanitizeUsersArray($users);
55 + $users = Helper::sanitizeUsersArray($users, $boardId);
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,70 +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
449 - $tasksQuery = Task::query()->where('parent_id', null)->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']);
450 - $isUserAdmin = PermissionManager::isAdmin($currentUserId);
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));
451 573
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();
574 + // Build base queries
575 + $firstThreeChars = substr($query, 0, 3);
576 + $firstNineChars = substr($query, 0, 9);
454 577
578 + if($firstThreeChars == 'id:') {
579 + $idPart = preg_replace('/[^a-zA-Z0-9]/', '', substr($query, 3));
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 . '%']);
586 + } else {
587 + $tasksQuery = Task::query()->where('parent_id', null)->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']);
588 + $boardQuery = Board::query()->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']);
589 + }
590 +
591 + // Apply scope and permissions
592 + $isUserAdmin = PermissionManager::isAdmin($currentUserId);
455 593 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 {
594 + if (!$isUserAdmin) {
461 595 $boardIds = PermissionManager::getBoardIdsForUser($currentUserId);
462 - $boards = $boardQuery->whereIn('id', $boardIds)->get();
463 - $tasks = $tasksQuery->whereIn('board_id', $boardIds)->get();
596 + $boardQuery->whereIn('id', $boardIds);
597 + $tasksQuery->whereIn('board_id', $boardIds);
464 598 }
465 599 } 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();
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);
470 606 } else {
471 - // Out of permission scope search
472 - $tasks = [];
607 + $tasksQuery->where('id', -1); // Force no results
473 608 }
474 609 }
475 610
611 + $allActiveBoardsIds = Board::query()->where('archived_at', null)->pluck('id')->toArray();
476 612
613 + $boards = [];
614 + $tasks = [];
615 + $totalBoards = 0;
616 + $totalTasks = 0;
617 + $formattedBoards = [];
477 618 $formattedTasks = [];
478 - $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 +
479 634 foreach ($boards as $board) {
480 635 $formattedBoards[] = [
481 636 'type' => 'board',
482 637 'id' => $board->id,
483 638 'title' => $board->title,
484 - 'description' => $board->description,
485 - 'url' => Helper::getBoardUrl($board->id)
639 + 'description' => DescriptionMarkdownConverter::normalize($board->description),
486 640 ];
487 641 }
488 642 foreach ($tasks as $task) {
489 -
490 643 if (!in_array($task->board_id, $allActiveBoardsIds)) {
491 - // if the task is not in an active board, skip it
492 644 continue;
493 645 }
494 646
495 647 $board = $task->board;
496 -
497 648 $formattedTasks[] = [
498 649 'type' => 'task',
499 650 'id' => $task->id,
500 651 'title' => $task->title,
501 - 'description' => $task->description,
502 - 'url' => Helper::getTaskUrl($task->id, $board->id),
652 + 'description' => DescriptionMarkdownConverter::normalize($task->description),
653 + 'board_id' => $task->board_id,
503 654 'board' => [
504 655 'id' => $board->id,
505 656 'title' => $board->title,
506 657 'url' => Helper::getBoardUrl($board->id)
@@ -508,51 +659,101 @@
508 659 'stage' => [
509 660 'id' => $task->stage_id,
510 661 'title' => $task->stage->title ?? '',
511 662 ],
512 -
513 663 ];
514 664 }
665 +
515 666 return $this->sendSuccess([
516 - 'tasks' => $formattedTasks,
517 - '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 + ]
518 681 ], 200);
519 -
520 682 }
521 683
522 - public function getDashboardViewSettings()
684 + public function getDashboardViewSettings(Request $request)
523 685 {
524 - try {
686 + $view = $request->getSafe('view', 'sanitize_text_field');
687 +
688 + if ($view == 'kanbanview') {
525 689 $globalSettings = $this->optionService->getDashboardViewSettings();
526 - if ($globalSettings->value)
527 - $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 + }
528 698
529 - return $this->sendSuccess([
530 - 'currentSettings' => $currentSettings,
531 - ], 200);
532 - } catch (\Exception $e) {
533 - return $this->sendError($e->getMessage(), 404);
534 - }
699 + if ($globalSettings->value)
700 + $currentSettings = maybe_unserialize($globalSettings->value);
701 +
702 + return $this->sendSuccess([
703 + 'currentSettings' => $currentSettings,
704 + ], 200);
535 705 }
536 706
537 707 public function updateDashboardViewSettings(Request $request)
538 708 {
539 - try {
540 - $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');
541 727
542 - $this->optionService->updateDashboardViewSettings($newSettings);
728 + $this->optionService->updateDashboardViewSettings($newSettings, $view);
543 729
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);
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');
549 736 }
737 +
738 + return $this->sendSuccess([
739 + 'message' => $message,
740 + ], 201);
550 741 }
551 742
552 743
553 744 public function getAddonsSettings()
554 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 +
555 756 $addOns = [
556 757 'fluent-crm' => [
557 758 'title' => __('FluentCRM', 'fluent-boards'),
558 759 'logo' => fluent_boards_mix('images/addons/fluent-crm.svg'),
@@ -559,9 +760,10 @@
559 760 'is_installed' => defined('FLUENTCRM'),
560 761 'learn_more_url' => 'https://fluentcrm.com/',
561 762 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluentcrm/',
562 763 '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')
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')
564 766 ],
565 767 'fluentform' => [
566 768 'title' => __('Fluent Forms', 'fluent-boards'),
567 769 'logo' => fluent_boards_mix('images/addons/fluentform.png'),
@@ -568,9 +770,10 @@
568 770 'is_installed' => defined('FLUENTFORM'),
569 771 'learn_more_url' => 'https://wordpress.org/plugins/fluentform/',
570 772 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluent-forms/',
571 773 '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')
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')
573 776 ],
574 777 'fluent-support' => [
575 778 'title' => __('Fluent Support', 'fluent-boards'),
576 779 'logo' => fluent_boards_mix('images/addons/fluent-support.svg'),
@@ -578,9 +781,10 @@
578 781 'learn_more_url' => 'https://wordpress.org/plugins/fluent-connect/',
579 782 'settings_url' => admin_url('admin.php?page=fluent-support#/'),
580 783 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluentsupport/',
581 784 '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')
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')
583 787 ],
584 788 'fluent-smtp' => [
585 789 'title' => __('Fluent SMTP', 'fluent-boards'),
586 790 'logo' => fluent_boards_mix('images/addons/fluent-smtp.svg'),
@@ -587,12 +791,28 @@
587 791 'is_installed' => defined('FLUENTMAIL'),
588 792 'learn_more_url' => 'https://wordpress.org/plugins/fluent-smtp/',
589 793 'associate_doc' => admin_url('options-general.php?page=fluent-mail#/'),
590 794 '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')
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')
592 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 + ],
593 811 ];
594 812
813 + $addOns = apply_filters('fluent_boards/addons_settings', $addOns);
814 +
595 815 $modules = fluent_boards_get_pref_settings(false);
596 816
597 817 if (empty($modules['frontend']['render_type'])) {
598 818 $modules['frontend']['render_type'] = 'standalone';
@@ -613,16 +833,27 @@
613 833 'message' => __('This feature is only available in Fluent Boards Pro', 'fluent-boards')
614 834 ]);
615 835 }
616 836
617 - $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 + }
618 845
619 846 $prefSettings = fluent_boards_get_pref_settings(false);
620 847
621 - $settings = wp_parse_args($settings, $prefSettings);
848 + $settings = wp_parse_args($rawSettings, $prefSettings);
622 849
623 850 $settings = Arr::only($settings, array_keys($prefSettings));
624 - $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 + }
625 856
626 857 if (empty($settings['frontend']['slug'])) {
627 858 $settings['frontend']['slug'] = 'projects';
628 859 }
@@ -634,8 +865,12 @@
634 865 do_action('fluent_boards/saving_addons', $settings, $prefSettings);
635 866
636 867 update_option('fluent_boards_modules', $settings, 'yes');
637 868
869 + if (isset($settings['recurring_task']['enabled']) && $settings['recurring_task']['enabled'] == 'no') {
870 + do_action('fluent_boards/recurring_task_disabled');
871 + }
872 +
638 873 return $this->sendSuccess([
639 874 'message' => __('Settings are saved', 'fluent-boards'),
640 875 'featureModules' => $settings
641 876 ]);
@@ -644,15 +879,15 @@
644 879 public function installPlugin(Request $request)
645 880 {
646 881 if (!current_user_can('install_plugins')) {
647 882 return $this->sendError([
648 - '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')
649 884 ]);
650 885 }
651 886
652 887 $plugin = $request->getSafe('plugin', 'sanitize_text_field');
653 888
654 - $acceptedPlugins = [
889 + $acceptedFreePlugins = [
655 890 'fluent-crm' => 'fluent-crm.php',
656 891 'fluentform' => 'fluentform.php',
657 892 'fluent-support' => 'fluent-support.php',
658 893 'fluent-smtp' => 'fluent-smtp.php'
@@ -657,8 +892,10 @@
657 892 'fluent-support' => 'fluent-support.php',
658 893 'fluent-smtp' => 'fluent-smtp.php'
659 894 ];
660 895
896 + $acceptedPlugins = apply_filters('fluent_boards/accepted_plugins', $acceptedFreePlugins);
897 +
661 898 if (!isset($acceptedPlugins[$plugin])) {
662 899 return $this->sendError([
663 900 'message' => __('Invalid plugin', 'fluent-boards')
664 901 ]);
@@ -663,9 +900,8 @@
663 900 'message' => __('Invalid plugin', 'fluent-boards')
664 901 ]);
665 902 }
666 903
667 -
668 904 $pluginToInstall = [
669 905 'name' => __('Fluent Plugin', 'fluent-boards'),
670 906 'repo-slug' => $plugin,
671 907 'file' => $acceptedPlugins[$plugin],
@@ -670,9 +906,14 @@
670 906 'repo-slug' => $plugin,
671 907 'file' => $acceptedPlugins[$plugin],
672 908 ];
673 909
674 - $this->backgroundInstaller($pluginToInstall, $plugin);
910 + // if plugin in free list then run background intaller otherwise call an action to install
911 + if (isset($acceptedFreePlugins[$plugin])) {
912 + $this->backgroundInstaller($pluginToInstall, $plugin);
913 + } else {
914 + do_action('fluent_boards/install_plugin', $pluginToInstall, $plugin);
915 + }
675 916
676 917 return $this->sendSuccess([
677 918 'message' => __('Plugin is being installed', 'fluent-boards')
678 919 ]);
@@ -682,8 +923,19 @@
682 923 {
683 924 return file_exists(WP_PLUGIN_DIR . '/' . $plugin);
684 925 }
685 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 +
686 938 private function backgroundInstaller($plugin_to_install, $plugin_id)
687 939 {
688 940 if (!empty($plugin_to_install['repo-slug'])) {
689 941 require_once ABSPATH . 'wp-admin/includes/file.php';
@@ -735,9 +987,9 @@
735 987 )
736 988 );
737 989
738 990 if (is_wp_error($plugin_information)) {
739 - throw new \Exception($plugin_information->get_error_message());
991 + throw new \Exception(esc_html($plugin_information->get_error_message()));
740 992 }
741 993
742 994 $package = $plugin_information->download_link;
743 995 $download = $upgrader->download_package($package);
@@ -742,15 +994,15 @@
742 994 $package = $plugin_information->download_link;
743 995 $download = $upgrader->download_package($package);
744 996
745 997 if (is_wp_error($download)) {
746 - throw new \Exception($download->get_error_message());
998 + throw new \Exception(esc_html($download->get_error_message()));
747 999 }
748 1000
749 1001 $working_dir = $upgrader->unpack_package($download, true);
750 1002
751 1003 if (is_wp_error($working_dir)) {
752 - throw new \Exception($working_dir->get_error_message());
1004 + throw new \Exception(esc_html($working_dir->get_error_message()));
753 1005 }
754 1006
755 1007 $result = $upgrader->install_package(
756 1008 array(
@@ -766,9 +1018,9 @@
766 1018 )
767 1019 );
768 1020
769 1021 if (is_wp_error($result)) {
770 - throw new \Exception($result->get_error_message());
1022 + throw new \Exception(esc_html($result->get_error_message()));
771 1023 }
772 1024
773 1025 $activate = true;
774 1026
@@ -786,9 +1038,9 @@
786 1038 try {
787 1039 $result = activate_plugin($installed ? $installed_plugins[$plugin_file] : $plugin_slug . '/' . $plugin_file);
788 1040
789 1041 if (is_wp_error($result)) {
790 - throw new \Exception($result->get_error_message());
1042 + throw new \Exception(esc_html($result->get_error_message()));
791 1043 }
792 1044 } catch (\Exception $e) {
793 1045 }
794 1046 }
@@ -829,9 +1081,10 @@
829 1081 $pages = [];
830 1082 foreach ($allPages as $page) {
831 1083 $pages[] = [
832 1084 'id' => $page->ID,
833 - '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))
834 1087 ];
835 1088 }
836 1089
837 1090 return $this->sendSuccess([
@@ -844,9 +1097,9 @@
844 1097 $settings = fluent_boards_get_option('general_settings', []);
845 1098
846 1099 return $this->sendSuccess([
847 1100 'settings' => $settings,
848 - 'server_timezone' => get_option('timezone_string')
1101 + 'server_timezone' => \wp_timezone_string()
849 1102 ]);
850 1103
851 1104 }
852 1105
@@ -851,18 +1104,45 @@
851 1104 }
852 1105
853 1106 public function saveGeneralSettings(Request $request)
854 1107 {
855 - $settings = $request->getSafe('updatedSettings', []);
1108 + // check for pro version
1109 + if (!defined('FLUENT_BOARDS_PRO')) {
1110 + return $this->sendError([
1111 + 'message' => __('This feature is only available in Fluent Boards Pro. Please upgrade.', 'fluent-boards')
1112 + ]);
1113 + }
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 + }
856 1131
857 1132 $settings = apply_filters('fluent_boards/save_general_settings', $settings);
858 1133
859 - $settings = fluent_boards_update_option('general_settings', $settings);
860 - $savedGeneralSettings = \maybe_unserialize($settings->value);
1134 + $savedSettings = fluent_boards_update_option('general_settings', $settings);
1135 + $savedGeneralSettings = \maybe_unserialize($savedSettings->value);
861 1136
862 - if ($savedGeneralSettings['daily_reminder_enabled'] || $savedGeneralSettings['daily_reminder_enabled'] == 'true') {
1137 + $scheduleHandler = new ProScheduleHandler();
1138 +
1139 + $dailyReminderEnabled = $savedGeneralSettings['daily_reminder_enabled'] ?? false;
1140 +
1141 + if (filter_var($dailyReminderEnabled, FILTER_VALIDATE_BOOLEAN)) {
863 1142 // force schedule from this settings update
864 - (new ProScheduleHandler())->schedule();
1143 + $scheduleHandler->clearDailyTaskReminderScheduler();
1144 + $scheduleHandler->scheduleDailyTaskReminder();
865 1145 }
866 1146
867 1147 return $this->sendSuccess([
868 1148 'settings' => $savedGeneralSettings,