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 +341 -106 1.35 → 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,94 +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);
450 576 $firstNineChars = substr($query, 0, 9);
451 577
452 578 if($firstThreeChars == 'id:') {
453 - $idPart = substr($query, 3);
454 - $idPart = preg_replace('/[^a-zA-Z0-9]/', '', $idPart);
579 + $idPart = preg_replace('/[^a-zA-Z0-9]/', '', substr($query, 3));
455 580 $tasksQuery = Task::query()->where('parent_id', null)->whereRaw('id LIKE ?', ['%' . $idPart . '%']);
581 + $boardQuery = Board::query()->whereRaw('id LIKE ?', ['%' . $idPart . '%']);
456 582 }elseif($firstNineChars == 'archived:') {
457 - $archivedPart = substr($query, 9);
458 - $archivedPart = trim($archivedPart);
583 + $archivedPart = trim(substr($query, 9));
459 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 . '%']);
460 586 } else {
461 587 $tasksQuery = Task::query()->where('parent_id', null)->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']);
588 + $boardQuery = Board::query()->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']);
462 589 }
463 590
591 + // Apply scope and permissions
464 592 $isUserAdmin = PermissionManager::isAdmin($currentUserId);
465 -
466 - // This is a check for deleted tasks to be excluded from search results
467 - $allActiveBoardsIds = Board::query()->where('archived_at', null)->pluck('id')->toArray();
468 -
469 593 if ($scope == 'all') {
470 - if($firstThreeChars == 'id:'){
471 - $idPart = substr($query, 3);
472 - $idPart = preg_replace('/[^a-zA-Z0-9]/', '', $idPart);
473 - $boardQuery = Board::query()->whereRaw('id LIKE ?', ['%' . $idPart . '%']);
474 - }elseif ($firstNineChars == 'archived:') {
475 - $archivedPart = substr($query, 9);
476 - $archivedPart = trim($archivedPart);
477 - $boardQuery = Board::query()->whereNotNull('archived_at')->whereRaw('LOWER(title) LIKE ?', ['%' . $archivedPart . '%']);
478 - } else {
479 - $boardQuery = Board::query()->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']);
480 - }
481 - if ($isUserAdmin) {
482 - $boards = $boardQuery->limit(10)->get();
483 - $tasks = $tasksQuery->limit(10)->get();
484 - } else {
594 + if (!$isUserAdmin) {
485 595 $boardIds = PermissionManager::getBoardIdsForUser($currentUserId);
486 - $boards = $boardQuery->whereIn('id', $boardIds)->limit(10)->get();
487 - $tasks = $tasksQuery->whereIn('board_id', $boardIds)->limit(10)->get();
596 + $boardQuery->whereIn('id', $boardIds);
597 + $tasksQuery->whereIn('board_id', $boardIds);
488 598 }
489 599 } else {
490 - $boards = []; // boards results is not needed in scoped search
491 - $inBoard = (int)$scope;
492 - if ($isUserAdmin || in_array($inBoard, $boardIds = PermissionManager::getBoardIdsForUser($currentUserId))) {
493 - $tasks = $tasksQuery->where('board_id', $inBoard)->limit(10)->get();
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);
494 606 } else {
495 - // Out of permission scope search
496 - $tasks = [];
607 + $tasksQuery->where('id', -1); // Force no results
497 608 }
498 609 }
499 610
611 + $allActiveBoardsIds = Board::query()->where('archived_at', null)->pluck('id')->toArray();
500 612
613 + $boards = [];
614 + $tasks = [];
615 + $totalBoards = 0;
616 + $totalTasks = 0;
617 + $formattedBoards = [];
501 618 $formattedTasks = [];
502 - $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 +
503 634 foreach ($boards as $board) {
504 635 $formattedBoards[] = [
505 636 'type' => 'board',
506 637 'id' => $board->id,
507 638 'title' => $board->title,
508 - 'description' => $board->description,
509 - 'url' => Helper::getBoardUrl($board->id)
639 + 'description' => DescriptionMarkdownConverter::normalize($board->description),
510 640 ];
511 641 }
512 642 foreach ($tasks as $task) {
513 -
514 643 if (!in_array($task->board_id, $allActiveBoardsIds)) {
515 - // if the task is not in an active board, skip it
516 644 continue;
517 645 }
518 646
519 647 $board = $task->board;
520 -
521 648 $formattedTasks[] = [
522 649 'type' => 'task',
523 650 'id' => $task->id,
524 651 'title' => $task->title,
525 - 'description' => $task->description,
526 - 'url' => Helper::getTaskUrl($task->id, $board->id),
652 + 'description' => DescriptionMarkdownConverter::normalize($task->description),
653 + 'board_id' => $task->board_id,
527 654 'board' => [
528 655 'id' => $board->id,
529 656 'title' => $board->title,
530 657 'url' => Helper::getBoardUrl($board->id)
@@ -532,51 +659,101 @@
532 659 'stage' => [
533 660 'id' => $task->stage_id,
534 661 'title' => $task->stage->title ?? '',
535 662 ],
536 -
537 663 ];
538 664 }
665 +
539 666 return $this->sendSuccess([
540 - 'tasks' => $formattedTasks,
541 - '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 + ]
542 681 ], 200);
543 -
544 682 }
545 683
546 - public function getDashboardViewSettings()
684 + public function getDashboardViewSettings(Request $request)
547 685 {
548 - try {
686 + $view = $request->getSafe('view', 'sanitize_text_field');
687 +
688 + if ($view == 'kanbanview') {
549 689 $globalSettings = $this->optionService->getDashboardViewSettings();
550 - if ($globalSettings->value)
551 - $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 + }
552 698
553 - return $this->sendSuccess([
554 - 'currentSettings' => $currentSettings,
555 - ], 200);
556 - } catch (\Exception $e) {
557 - return $this->sendError($e->getMessage(), 404);
558 - }
699 + if ($globalSettings->value)
700 + $currentSettings = maybe_unserialize($globalSettings->value);
701 +
702 + return $this->sendSuccess([
703 + 'currentSettings' => $currentSettings,
704 + ], 200);
559 705 }
560 706
561 707 public function updateDashboardViewSettings(Request $request)
562 708 {
563 - try {
564 - $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');
565 727
566 - $this->optionService->updateDashboardViewSettings($newSettings);
728 + $this->optionService->updateDashboardViewSettings($newSettings, $view);
567 729
568 - return $this->sendSuccess([
569 - 'message' => __("Dashboard view settings are updated", 'fluent-boards'),
570 - ], 201);
571 - } catch (\Exception $e) {
572 - return $this->sendError($e->getMessage(), 404);
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');
573 736 }
737 +
738 + return $this->sendSuccess([
739 + 'message' => $message,
740 + ], 201);
574 741 }
575 742
576 743
577 744 public function getAddonsSettings()
578 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 +
579 756 $addOns = [
580 757 'fluent-crm' => [
581 758 'title' => __('FluentCRM', 'fluent-boards'),
582 759 'logo' => fluent_boards_mix('images/addons/fluent-crm.svg'),
@@ -583,9 +760,10 @@
583 760 'is_installed' => defined('FLUENTCRM'),
584 761 'learn_more_url' => 'https://fluentcrm.com/',
585 762 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluentcrm/',
586 763 'action_text' => $this->isPluginInstalled('fluent-crm/fluent-crm.php') ? __('Activate FluentCRM', 'fluent-boards') : __('Install FluentCRM', 'fluent-boards'),
587 - 'description' => __('FluentCRM is a Self Hosted Email Marketing Automation Plugin for WordPress. Manage your leads and customers, email campaigns, automated email sequencing and many more', 'fluent-boards')
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')
588 766 ],
589 767 'fluentform' => [
590 768 'title' => __('Fluent Forms', 'fluent-boards'),
591 769 'logo' => fluent_boards_mix('images/addons/fluentform.png'),
@@ -592,9 +770,10 @@
592 770 'is_installed' => defined('FLUENTFORM'),
593 771 'learn_more_url' => 'https://wordpress.org/plugins/fluentform/',
594 772 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluent-forms/',
595 773 'action_text' => $this->isPluginInstalled('fluent-form/fluent-form.php') ? __('Activate Fluent Forms', 'fluent-boards') : __('Install Fluent Forms', 'fluent-boards'),
596 - 'description' => __('Collect leads and build any type of forms, accept payments, connect with your CRM with the Fastest Contact Form Builder Plugin for WordPress', 'fluent-boards')
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')
597 776 ],
598 777 'fluent-support' => [
599 778 'title' => __('Fluent Support', 'fluent-boards'),
600 779 'logo' => fluent_boards_mix('images/addons/fluent-support.svg'),
@@ -602,9 +781,10 @@
602 781 'learn_more_url' => 'https://wordpress.org/plugins/fluent-connect/',
603 782 'settings_url' => admin_url('admin.php?page=fluent-support#/'),
604 783 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluentsupport/',
605 784 'action_text' => $this->isPluginInstalled('fluent-support/fluent-support.php') ? __('Activate Fluent Support', 'fluent-boards') : __('Install Fluent Support', 'fluent-boards'),
606 - 'description' => __('WordPress Helpdesk and Customer Support Ticket Plugin. Provide awesome support and manage customer queries right from your WordPress dashboard.', 'fluent-boards')
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')
607 787 ],
608 788 'fluent-smtp' => [
609 789 'title' => __('Fluent SMTP', 'fluent-boards'),
610 790 'logo' => fluent_boards_mix('images/addons/fluent-smtp.svg'),
@@ -611,10 +791,24 @@
611 791 'is_installed' => defined('FLUENTMAIL'),
612 792 'learn_more_url' => 'https://wordpress.org/plugins/fluent-smtp/',
613 793 'associate_doc' => admin_url('options-general.php?page=fluent-mail#/'),
614 794 'action_text' => $this->isPluginInstalled('fluent-smtp/fluent-smtp.php') ? __('Activate Fluent SMTP', 'fluent-boards') : __('Install Fluent SMTP', 'fluent-boards'),
615 - 'description' => __('The Ultimate SMTP and SES Plugin for WordPress. Connect with any SMTP, SendGrid, Mailgun, SES, Sendinblue, PepiPost, Google, Microsoft and more.', 'fluent-boards')
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')
616 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 + ],
617 811 ];
618 812
619 813 $addOns = apply_filters('fluent_boards/addons_settings', $addOns);
620 814
@@ -639,16 +833,27 @@
639 833 'message' => __('This feature is only available in Fluent Boards Pro', 'fluent-boards')
640 834 ]);
641 835 }
642 836
643 - $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 + }
644 845
645 846 $prefSettings = fluent_boards_get_pref_settings(false);
646 847
647 - $settings = wp_parse_args($settings, $prefSettings);
848 + $settings = wp_parse_args($rawSettings, $prefSettings);
648 849
649 850 $settings = Arr::only($settings, array_keys($prefSettings));
650 - $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 + }
651 856
652 857 if (empty($settings['frontend']['slug'])) {
653 858 $settings['frontend']['slug'] = 'projects';
654 859 }
@@ -674,9 +879,9 @@
674 879 public function installPlugin(Request $request)
675 880 {
676 881 if (!current_user_can('install_plugins')) {
677 882 return $this->sendError([
678 - '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')
679 884 ]);
680 885 }
681 886
682 887 $plugin = $request->getSafe('plugin', 'sanitize_text_field');
@@ -718,8 +923,19 @@
718 923 {
719 924 return file_exists(WP_PLUGIN_DIR . '/' . $plugin);
720 925 }
721 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 +
722 938 private function backgroundInstaller($plugin_to_install, $plugin_id)
723 939 {
724 940 if (!empty($plugin_to_install['repo-slug'])) {
725 941 require_once ABSPATH . 'wp-admin/includes/file.php';
@@ -771,9 +987,9 @@
771 987 )
772 988 );
773 989
774 990 if (is_wp_error($plugin_information)) {
775 - throw new \Exception($plugin_information->get_error_message());
991 + throw new \Exception(esc_html($plugin_information->get_error_message()));
776 992 }
777 993
778 994 $package = $plugin_information->download_link;
779 995 $download = $upgrader->download_package($package);
@@ -778,15 +994,15 @@
778 994 $package = $plugin_information->download_link;
779 995 $download = $upgrader->download_package($package);
780 996
781 997 if (is_wp_error($download)) {
782 - throw new \Exception($download->get_error_message());
998 + throw new \Exception(esc_html($download->get_error_message()));
783 999 }
784 1000
785 1001 $working_dir = $upgrader->unpack_package($download, true);
786 1002
787 1003 if (is_wp_error($working_dir)) {
788 - throw new \Exception($working_dir->get_error_message());
1004 + throw new \Exception(esc_html($working_dir->get_error_message()));
789 1005 }
790 1006
791 1007 $result = $upgrader->install_package(
792 1008 array(
@@ -802,9 +1018,9 @@
802 1018 )
803 1019 );
804 1020
805 1021 if (is_wp_error($result)) {
806 - throw new \Exception($result->get_error_message());
1022 + throw new \Exception(esc_html($result->get_error_message()));
807 1023 }
808 1024
809 1025 $activate = true;
810 1026
@@ -822,9 +1038,9 @@
822 1038 try {
823 1039 $result = activate_plugin($installed ? $installed_plugins[$plugin_file] : $plugin_slug . '/' . $plugin_file);
824 1040
825 1041 if (is_wp_error($result)) {
826 - throw new \Exception($result->get_error_message());
1042 + throw new \Exception(esc_html($result->get_error_message()));
827 1043 }
828 1044 } catch (\Exception $e) {
829 1045 }
830 1046 }
@@ -865,9 +1081,10 @@
865 1081 $pages = [];
866 1082 foreach ($allPages as $page) {
867 1083 $pages[] = [
868 1084 'id' => $page->ID,
869 - '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))
870 1087 ];
871 1088 }
872 1089
873 1090 return $this->sendSuccess([
@@ -893,9 +1110,25 @@
893 1110 return $this->sendError([
894 1111 'message' => __('This feature is only available in Fluent Boards Pro. Please upgrade.', 'fluent-boards')
895 1112 ]);
896 1113 }
897 - $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 + }
898 1131
899 1132 $settings = apply_filters('fluent_boards/save_general_settings', $settings);
900 1133
901 1134 $savedSettings = fluent_boards_update_option('general_settings', $settings);
@@ -902,9 +1135,11 @@
902 1135 $savedGeneralSettings = \maybe_unserialize($savedSettings->value);
903 1136
904 1137 $scheduleHandler = new ProScheduleHandler();
905 1138
906 - 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)) {
907 1142 // force schedule from this settings update
908 1143 $scheduleHandler->clearDailyTaskReminderScheduler();
909 1144 $scheduleHandler->scheduleDailyTaskReminder();
910 1145 }