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 +345 -106 1.30 → 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->get();
483 - $tasks = $tasksQuery->get();
484 - } else {
594 + if (!$isUserAdmin) {
485 595 $boardIds = PermissionManager::getBoardIdsForUser($currentUserId);
486 - $boards = $boardQuery->whereIn('id', $boardIds)->get();
487 - $tasks = $tasksQuery->whereIn('board_id', $boardIds)->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)->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 }
@@ -660,8 +865,12 @@
660 865 do_action('fluent_boards/saving_addons', $settings, $prefSettings);
661 866
662 867 update_option('fluent_boards_modules', $settings, 'yes');
663 868
869 + if (isset($settings['recurring_task']['enabled']) && $settings['recurring_task']['enabled'] == 'no') {
870 + do_action('fluent_boards/recurring_task_disabled');
871 + }
872 +
664 873 return $this->sendSuccess([
665 874 'message' => __('Settings are saved', 'fluent-boards'),
666 875 'featureModules' => $settings
667 876 ]);
@@ -670,9 +879,9 @@
670 879 public function installPlugin(Request $request)
671 880 {
672 881 if (!current_user_can('install_plugins')) {
673 882 return $this->sendError([
674 - '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')
675 884 ]);
676 885 }
677 886
678 887 $plugin = $request->getSafe('plugin', 'sanitize_text_field');
@@ -714,8 +923,19 @@
714 923 {
715 924 return file_exists(WP_PLUGIN_DIR . '/' . $plugin);
716 925 }
717 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 +
718 938 private function backgroundInstaller($plugin_to_install, $plugin_id)
719 939 {
720 940 if (!empty($plugin_to_install['repo-slug'])) {
721 941 require_once ABSPATH . 'wp-admin/includes/file.php';
@@ -767,9 +987,9 @@
767 987 )
768 988 );
769 989
770 990 if (is_wp_error($plugin_information)) {
771 - throw new \Exception($plugin_information->get_error_message());
991 + throw new \Exception(esc_html($plugin_information->get_error_message()));
772 992 }
773 993
774 994 $package = $plugin_information->download_link;
775 995 $download = $upgrader->download_package($package);
@@ -774,15 +994,15 @@
774 994 $package = $plugin_information->download_link;
775 995 $download = $upgrader->download_package($package);
776 996
777 997 if (is_wp_error($download)) {
778 - throw new \Exception($download->get_error_message());
998 + throw new \Exception(esc_html($download->get_error_message()));
779 999 }
780 1000
781 1001 $working_dir = $upgrader->unpack_package($download, true);
782 1002
783 1003 if (is_wp_error($working_dir)) {
784 - throw new \Exception($working_dir->get_error_message());
1004 + throw new \Exception(esc_html($working_dir->get_error_message()));
785 1005 }
786 1006
787 1007 $result = $upgrader->install_package(
788 1008 array(
@@ -798,9 +1018,9 @@
798 1018 )
799 1019 );
800 1020
801 1021 if (is_wp_error($result)) {
802 - throw new \Exception($result->get_error_message());
1022 + throw new \Exception(esc_html($result->get_error_message()));
803 1023 }
804 1024
805 1025 $activate = true;
806 1026
@@ -818,9 +1038,9 @@
818 1038 try {
819 1039 $result = activate_plugin($installed ? $installed_plugins[$plugin_file] : $plugin_slug . '/' . $plugin_file);
820 1040
821 1041 if (is_wp_error($result)) {
822 - throw new \Exception($result->get_error_message());
1042 + throw new \Exception(esc_html($result->get_error_message()));
823 1043 }
824 1044 } catch (\Exception $e) {
825 1045 }
826 1046 }
@@ -861,9 +1081,10 @@
861 1081 $pages = [];
862 1082 foreach ($allPages as $page) {
863 1083 $pages[] = [
864 1084 'id' => $page->ID,
865 - '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))
866 1087 ];
867 1088 }
868 1089
869 1090 return $this->sendSuccess([
@@ -889,9 +1110,25 @@
889 1110 return $this->sendError([
890 1111 'message' => __('This feature is only available in Fluent Boards Pro. Please upgrade.', 'fluent-boards')
891 1112 ]);
892 1113 }
893 - $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 + }
894 1131
895 1132 $settings = apply_filters('fluent_boards/save_general_settings', $settings);
896 1133
897 1134 $savedSettings = fluent_boards_update_option('general_settings', $settings);
@@ -898,9 +1135,11 @@
898 1135 $savedGeneralSettings = \maybe_unserialize($savedSettings->value);
899 1136
900 1137 $scheduleHandler = new ProScheduleHandler();
901 1138
902 - 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)) {
903 1142 // force schedule from this settings update
904 1143 $scheduleHandler->clearDailyTaskReminderScheduler();
905 1144 $scheduleHandler->scheduleDailyTaskReminder();
906 1145 }