PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / trunk
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration vtrunk
2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 1.40 1.41 1.45 All 41 releases
← All changes | app/Http/Controllers/OptionsController.php +334 -101 1.23trunk 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
@@ -56,11 +57,30 @@
56 57 }
57 58
58 59 $options = $this->addUserDataAsSelectorOption($users);
59 60
61 + } elseif ('board_create_users' === $optionKey) {
62 + if (!PermissionManager::userHasBoardCreationPermission()) {
63 + throw new \Exception(esc_html__('You do not have permission to access this route', 'fluent-boards'));
64 + }
65 +
66 + if (!defined('FLUENT_BOARDS_PRO')) {
67 + // Bound the lookup: this popover searches on focus (empty query too).
68 + $users = PermissionManager::getAll_WP_Admins($search, 20);
69 + } else {
70 + $users = Helper::searchWordPressUsers($search);
71 + }
72 +
73 + $options = $this->addUserDataAsSelectorOption($users);
74 + // Board creation can be delegated to members without `list_users`;
75 + // don't leak full account emails to them.
76 + $options = $this->maskSelectorEmailsForViewer($options);
77 +
60 78 } elseif ('boards' === $optionKey) {
61 79 $boards = Board::query()
80 + ->byAccessUser(get_current_user_id())
62 81 ->when($search, function ($query) use ($search) {
82 + // $search is already sanitized with sanitize_text_field above
63 83 return $query->where('title', 'LIKE', '%' . $search . '%');
64 84 })->take(20)->get();
65 85
66 86 foreach ($boards as $board) {
@@ -71,12 +91,18 @@
71 91 'right_side_value' => $board->slug,
72 92 ];
73 93 }
74 94 } elseif ('tasks' === $optionKey) {
95 + if (!PermissionManager::userHasPermission($boardId)) {
96 + throw new \Exception(esc_html__('You do not have permission to access this route', 'fluent-boards'));
97 + }
98 +
99 + // $boardId is already sanitized with intval above
75 100 $tasks = Task::where('board_id', $boardId)
76 101 ->whereNull('archived_at')
77 102 ->whereNull('parent_id')
78 103 ->when($search, function ($query) use ($search) {
104 + // $search is already sanitized with sanitize_text_field above
79 105 return $query->where('title', 'LIKE', '%' . $search . '%');
80 106 })->take(20)->get();
81 107
82 108 foreach ($tasks as $task) {
@@ -82,13 +108,18 @@
82 108 foreach ($tasks as $task) {
83 109 $options[] = [
84 110 'id' => $task->id,
85 111 'title' => $task->title,
86 - 'board_id' => $task->board_id
112 + 'board_id' => $task->board_id,
113 + 'subtask_groups' => $task->subtaskGroup
87 114 ];
88 115 }
89 116
90 117 } elseif ('assigned_in_task' == $optionKey) {
118 + if (!PermissionManager::userHasPermission($boardId)) {
119 + throw new \Exception(esc_html__('You do not have permission to access this route', 'fluent-boards'));
120 + }
121 +
91 122 $users = (new BoardService())->getAssigneesByBoard($boardId, $search);
92 123 $options = $this->addUserDataAsSelectorOption($users);
93 124 } else {
94 125 $options = apply_filters('fluent_boards/ajax_options_' . $optionKey, [], $search, $includedIds);
@@ -116,8 +147,42 @@
116 147 }
117 148 return $options;
118 149 }
119 150
151 + /**
152 + * Obfuscate emails in selector options for viewers who lack the `list_users`
153 + * capability, keeping the current user's own email visible.
154 + */
155 + private function maskSelectorEmailsForViewer($options)
156 + {
157 + if (current_user_can('list_users')) {
158 + return $options;
159 + }
160 +
161 + $currentUser = wp_get_current_user();
162 + $currentUserEmail = ($currentUser && isset($currentUser->user_email)) ? $currentUser->user_email : '';
163 +
164 + foreach ($options as $index => $option) {
165 + $email = $option['email'] ?? '';
166 +
167 + if ($email === '' || $email === $currentUserEmail) {
168 + continue;
169 + }
170 +
171 + $maskedEmail = Helper::obfuscateEmail($email);
172 +
173 + // When there's no display name the raw email doubles as the name.
174 + if (($option['name'] ?? '') === $email) {
175 + $options[$index]['name'] = $maskedEmail;
176 + }
177 +
178 + $options[$index]['email'] = $maskedEmail;
179 + $options[$index]['title'] = ($options[$index]['name'] ?? $maskedEmail) . ' (' . $maskedEmail . ')';
180 + }
181 +
182 + return $options;
183 + }
184 +
120 185 public function getCurrentUserPermissions()
121 186 {
122 187 try {
123 188 $currentUserBoards = Relation::query()
@@ -139,10 +204,10 @@
139 204
140 205 public function getUserPermission(Request $request)
141 206 {
142 207 try {
143 - $boardId = $request->getSafe('boardId');
144 - $userId = $request->getSafe('userId');
208 + $boardId = $request->getSafe('boardId', 'intval');
209 + $userId = $request->getSafe('userId', 'intval');
145 210
146 211 $boardUser = Relation::where('board_id', $boardId)
147 212 ->where('user_id', $userId)
148 213 ->where('status', 'ACTIVE')->first();
@@ -160,12 +225,12 @@
160 225
161 226 public function updatedUserPermission(Request $request)
162 227 {
163 228 try {
164 - $permission = $request->getSafe('userPermission');
165 - $updateType = $request->getSafe('updateType');
166 - $boardId = $request->getSafe('boardId');
167 - $userId = $request->getSafe('userId');
229 + $permission = $request->getSafe('userPermission', 'sanitize_text_field');
230 + $updateType = $request->getSafe('updateType', 'sanitize_text_field');
231 + $boardId = $request->getSafe('boardId', 'intval');
232 + $userId = $request->getSafe('userId', 'intval');
168 233
169 234 $boardUser = Relation::where('board_id', $boardId)->where('user_id', $userId)->status('ACTIVE')->first();
170 235
171 236 if ('Board Admin' == $permission) {
@@ -197,8 +262,9 @@
197 262 }
198 263
199 264 public function SetUserSuperAdmin($userId)
200 265 {
266 + $userId = absint($userId);
201 267 try {
202 268 $this->optionService->createSuperAdmin($userId);
203 269 return $this->sendSuccess([
204 270 'message' => __('Member has been set super admin successfully!', 'fluent-boards')
@@ -210,8 +276,9 @@
210 276 }
211 277
212 278 public function removeUserSuperAdmin($userId)
213 279 {
280 + $userId = absint($userId);
214 281 try {
215 282 $this->optionService->removeUserSuperAdmin($userId);
216 283
217 284 return $this->sendSuccess([
@@ -224,9 +291,9 @@
224 291
225 292 public function IsUserAllBoardAdmin(Request $request)
226 293 {
227 294 try {
228 - $userId = $request->getSafe('id');
295 + $userId = $request->getSafe('id', 'intval');
229 296 $isSuperAdmin = false;
230 297 $superAdmin = Relation::where('board_id', null)->where('user_id', $userId)->where('status', 'ACTIVE')->first();
231 298 $totalSuperAdmin = Relation::where('board_id', null)->where('status', 'ACTIVE')->count();
232 299 $permissions = [];
@@ -246,10 +313,11 @@
246 313 }
247 314
248 315 public function RemoveUserFromSuperAdmin(Request $request, $id)
249 316 {
317 + $id = absint($id);
250 318 try {
251 - $userId = $request->getSafe('id');
319 + $userId = $request->getSafe('id', 'intval');
252 320
253 321 $superAdmin = Relation::where('board_id', null)->where('user_id', $userId)->first();
254 322 $superAdmin->status = 'INACTIVE';
255 323 $superAdmin->save();
@@ -264,10 +332,10 @@
264 332
265 333 public function removeUserFromBoard(Request $request)
266 334 {
267 335 try {
268 - $boardId = $request->getSafe('boardId');
269 - $userId = $request->getSafe('userId');
336 + $boardId = $request->getSafe('boardId', 'intval');
337 + $userId = $request->getSafe('userId', 'intval');
270 338
271 339 $this->boardService->removeUserFromBoard($boardId, $userId);
272 340
273 341 if (!PermissionManager::isAdmin($userId)) {
@@ -284,9 +352,14 @@
284 352
285 353 public function addAsSuperAdmin(Request $request)
286 354 {
287 355 try {
288 - $userIds = $request->getSafe('memberIds');
356 + $rawUserIds = $request->getSafe('memberIds');
357 + // Sanitize array of user IDs
358 + $userIds = [];
359 + if (is_array($rawUserIds)) {
360 + $userIds = array_filter(array_map('intval', $rawUserIds));
361 + }
289 362 foreach ($userIds as $userId) {
290 363 $this->createSuperAdmin($userId);
291 364 }
292 365
@@ -315,10 +388,20 @@
315 388
316 389 public function addMembersInBoards(Request $request)
317 390 {
318 391 try {
319 - $userIds = $request->getSafe('memberIds');
320 - $boardIds = $request->getSafe('boardIds');
392 + $rawUserIds = $request->getSafe('memberIds');
393 + $rawBoardIds = $request->getSafe('boardIds');
394 +
395 + // Sanitize arrays of IDs
396 + $userIds = [];
397 + if (is_array($rawUserIds)) {
398 + $userIds = array_filter(array_map('intval', $rawUserIds));
399 + }
400 + $boardIds = [];
401 + if (is_array($rawBoardIds)) {
402 + $boardIds = array_filter(array_map('intval', $rawBoardIds));
403 + }
321 404
322 405 foreach ($userIds as $userId) {
323 406 foreach ($boardIds as $boardId) {
324 407 $this->boardService->addMembersInBoard($boardId, $userId);
@@ -336,9 +419,18 @@
336 419
337 420 public function updateGlobalNotificationSettings(Request $request)
338 421 {
339 422 try {
340 - $newSettings = $request->getSafe('updatedSettings');
423 + // updatedSettings is an array, sanitize each element
424 + $rawSettings = $request->get('updatedSettings');
425 + $newSettings = [];
426 + if (is_array($rawSettings)) {
427 + foreach ($rawSettings as $key => $value) {
428 + $sanitizedKey = sanitize_text_field($key);
429 + $sanitizedValue = sanitize_text_field($value);
430 + $newSettings[$sanitizedKey] = $sanitizedValue;
431 + }
432 + }
341 433
342 434 $this->optionService->updateGlobalNotificationSettings($newSettings);
343 435
344 436 return $this->sendSuccess([
@@ -373,21 +465,29 @@
373 465 }
374 466
375 467 $boardId = $request->getSafe('boardId', 'intval');
376 468
377 - $memberUserIds = Relation::where('object_type', 'board_user')
469 + if ($boardId && !PermissionManager::userHasPermission($boardId)) {
470 + return $this->sendError([
471 + 'message' => __('You do not have permission to access this route', 'fluent-boards')
472 + ], 403);
473 + }
474 +
475 + $memberUserIdsQuery = Relation::where('object_type', Constant::OBJECT_TYPE_BOARD_USER)
378 476 ->select(['foreign_id'])
379 477 ->groupBy('foreign_id');
380 478
381 479 if ($boardId) {
382 - $memberUserIds = $memberUserIds->where('object_id', $boardId);
480 + $memberUserIdsQuery->where('object_id', $boardId);
481 + } elseif (!PermissionManager::isAdmin()) {
482 + $boardIds = array_filter(array_map('intval', PermissionManager::getBoardIdsForUser()));
483 + $memberUserIdsQuery->whereIn('object_id', $boardIds);
383 484 }
384 485
385 - $members = [];
386 -
387 - $memberUserIds = $memberUserIds->get()
486 + $memberUserIds = $memberUserIdsQuery->get()
388 487 ->pluck('foreign_id')->toArray();
389 488
489 + $members = [];
390 490
391 491 if ($memberUserIds) {
392 492 $memberUsers = get_users([
393 493 'include' => $memberUserIds
@@ -437,85 +537,106 @@
437 537 'members' => $members
438 538 ];
439 539 }
440 540
441 - public function quickSearch()
541 + public function globalSearch()
442 542 {
443 543 $currentUserId = get_current_user_id();
444 544
445 - $query = sanitize_text_field($_REQUEST['query']);
446 - $query = strtolower($query);
447 - $scope = sanitize_text_field($_REQUEST['scope']);
545 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
546 + $query = isset($_REQUEST['query']) ? strtolower(sanitize_text_field(wp_unslash($_REQUEST['query']))) : '';
547 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
548 + $scope = isset($_REQUEST['scope']) ? sanitize_text_field(wp_unslash($_REQUEST['scope'])) : 'all';
448 549
550 + // Pagination parameters
551 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
552 + $taskPage = isset($_REQUEST['task_page']) ? max(1, (int)$_REQUEST['task_page']) : 0;
553 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
554 + $boardPage = isset($_REQUEST['board_page']) ? max(1, (int)$_REQUEST['board_page']) : 0;
555 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
556 + $perPage = isset($_REQUEST['per_page']) ? (int)$_REQUEST['per_page'] : 20;
557 + $perPage = max(1, min(100, $perPage));
558 +
559 + // Build base queries
449 560 $firstThreeChars = substr($query, 0, 3);
561 + $firstNineChars = substr($query, 0, 9);
450 562
451 563 if($firstThreeChars == 'id:') {
452 - $idPart = substr($query, 3);
453 - $idPart = preg_replace('/[^a-zA-Z0-9]/', '', $idPart);
564 + $idPart = preg_replace('/[^a-zA-Z0-9]/', '', substr($query, 3));
454 565 $tasksQuery = Task::query()->where('parent_id', null)->whereRaw('id LIKE ?', ['%' . $idPart . '%']);
566 + $boardQuery = Board::query()->whereRaw('id LIKE ?', ['%' . $idPart . '%']);
567 + }elseif($firstNineChars == 'archived:') {
568 + $archivedPart = trim(substr($query, 9));
569 + $tasksQuery = Task::query()->where('parent_id', null)->whereNotNull('archived_at')->whereRaw('LOWER(title) LIKE ?', ['%' . $archivedPart . '%']);
570 + $boardQuery = Board::query()->whereNotNull('archived_at')->whereRaw('LOWER(title) LIKE ?', ['%' . $archivedPart . '%']);
455 571 } else {
456 572 $tasksQuery = Task::query()->where('parent_id', null)->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']);
573 + $boardQuery = Board::query()->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']);
457 574 }
458 575
576 + // Apply scope and permissions
459 577 $isUserAdmin = PermissionManager::isAdmin($currentUserId);
460 -
461 - // This is a check for deleted tasks to be excluded from search results
462 - $allActiveBoardsIds = Board::query()->where('archived_at', null)->pluck('id')->toArray();
463 -
464 578 if ($scope == 'all') {
465 - if($firstThreeChars == 'id:'){
466 - $idPart = substr($query, 3);
467 - $idPart = preg_replace('/[^a-zA-Z0-9]/', '', $idPart);
468 - $boardQuery = Board::query()->whereRaw('id LIKE ?', ['%' . $idPart . '%']);
469 - } else {
470 - $boardQuery = Board::query()->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']);
471 - }
472 - if ($isUserAdmin) {
473 - $boards = $boardQuery->get();
474 - $tasks = $tasksQuery->get();
475 - } else {
579 + if (!$isUserAdmin) {
476 580 $boardIds = PermissionManager::getBoardIdsForUser($currentUserId);
477 - $boards = $boardQuery->whereIn('id', $boardIds)->get();
478 - $tasks = $tasksQuery->whereIn('board_id', $boardIds)->get();
581 + $boardQuery->whereIn('id', $boardIds);
582 + $tasksQuery->whereIn('board_id', $boardIds);
479 583 }
480 584 } else {
481 - $boards = []; // boards results is not needed in scoped search
482 - $inBoard = (int)$scope;
483 - if ($isUserAdmin || in_array($inBoard, $boardIds = PermissionManager::getBoardIdsForUser($currentUserId))) {
484 - $tasks = $tasksQuery->where('board_id', $inBoard)->get();
585 + // For 'current_board' scope, we don't search boards
586 + $boardQuery->where('id', -1);
587 +
588 + $inBoard = absint($scope);
589 + if ($isUserAdmin || in_array($inBoard, PermissionManager::getBoardIdsForUser($currentUserId))) {
590 + $tasksQuery->where('board_id', $inBoard);
485 591 } else {
486 - // Out of permission scope search
487 - $tasks = [];
592 + $tasksQuery->where('id', -1); // Force no results
488 593 }
489 594 }
490 595
596 + $allActiveBoardsIds = Board::query()->where('archived_at', null)->pluck('id')->toArray();
491 597
598 + $boards = [];
599 + $tasks = [];
600 + $totalBoards = 0;
601 + $totalTasks = 0;
602 + $formattedBoards = [];
492 603 $formattedTasks = [];
493 - $formattedBoards = [];
604 +
605 + // Fetch Boards if requested
606 + if ($boardPage > 0) {
607 + $totalBoards = $boardQuery->count();
608 + $boardOffset = ($boardPage - 1) * $perPage;
609 + $boards = $boardQuery->skip($boardOffset)->take($perPage)->get();
610 + }
611 +
612 + // Fetch Tasks if requested
613 + if ($taskPage > 0) {
614 + $totalTasks = $tasksQuery->count();
615 + $taskOffset = ($taskPage - 1) * $perPage;
616 + $tasks = $tasksQuery->skip($taskOffset)->take($perPage)->get();
617 + }
618 +
494 619 foreach ($boards as $board) {
495 620 $formattedBoards[] = [
496 621 'type' => 'board',
497 622 'id' => $board->id,
498 623 'title' => $board->title,
499 - 'description' => $board->description,
500 - 'url' => Helper::getBoardUrl($board->id)
624 + 'description' => DescriptionMarkdownConverter::normalize($board->description),
501 625 ];
502 626 }
503 627 foreach ($tasks as $task) {
504 -
505 628 if (!in_array($task->board_id, $allActiveBoardsIds)) {
506 - // if the task is not in an active board, skip it
507 629 continue;
508 630 }
509 631
510 632 $board = $task->board;
511 -
512 633 $formattedTasks[] = [
513 634 'type' => 'task',
514 635 'id' => $task->id,
515 636 'title' => $task->title,
516 - 'description' => $task->description,
517 - 'url' => Helper::getTaskUrl($task->id, $board->id),
637 + 'description' => DescriptionMarkdownConverter::normalize($task->description),
638 + 'board_id' => $task->board_id,
518 639 'board' => [
519 640 'id' => $board->id,
520 641 'title' => $board->title,
521 642 'url' => Helper::getBoardUrl($board->id)
@@ -523,51 +644,101 @@
523 644 'stage' => [
524 645 'id' => $task->stage_id,
525 646 'title' => $task->stage->title ?? '',
526 647 ],
527 -
528 648 ];
529 649 }
650 +
530 651 return $this->sendSuccess([
531 - 'tasks' => $formattedTasks,
532 - 'boards' => $formattedBoards
652 + 'tasks' => [
653 + 'data' => $formattedTasks,
654 + 'current_page' => $taskPage,
655 + 'per_page' => $perPage,
656 + 'total' => $totalTasks,
657 + 'last_page' => (int) ceil($totalTasks / $perPage)
658 + ],
659 + 'boards' => [
660 + 'data' => $formattedBoards,
661 + 'current_page' => $boardPage,
662 + 'per_page' => $perPage,
663 + 'total' => $totalBoards,
664 + 'last_page' => (int) ceil($totalBoards / $perPage)
665 + ]
533 666 ], 200);
534 -
535 667 }
536 668
537 - public function getDashboardViewSettings()
669 + public function getDashboardViewSettings(Request $request)
538 670 {
539 - try {
671 + $view = $request->getSafe('view', 'sanitize_text_field');
672 +
673 + if ($view == 'kanbanview') {
540 674 $globalSettings = $this->optionService->getDashboardViewSettings();
541 - if ($globalSettings->value)
542 - $currentSettings = maybe_unserialize($globalSettings->value);
675 + } elseif ($view == 'listview') {
676 + $globalSettings = $this->optionService->getListViewPreferences();
677 + } elseif ($view == 'tableview') {
678 + $globalSettings = $this->optionService->getTableViewPreferences();
679 + } else {
680 + // Handle invalid view or default to one
681 + return $this->sendError(['message' => __('Invalid view type', 'fluent-boards')], 400);
682 + }
543 683
544 - return $this->sendSuccess([
545 - 'currentSettings' => $currentSettings,
546 - ], 200);
547 - } catch (\Exception $e) {
548 - return $this->sendError($e->getMessage(), 404);
549 - }
684 + if ($globalSettings->value)
685 + $currentSettings = maybe_unserialize($globalSettings->value);
686 +
687 + return $this->sendSuccess([
688 + 'currentSettings' => $currentSettings,
689 + ], 200);
550 690 }
551 691
552 692 public function updateDashboardViewSettings(Request $request)
553 693 {
554 - try {
555 - $newSettings = $request->getSafe('updatedSettings');
694 + // updatedSettings is an array, sanitize each element
695 + $rawSettings = $request->get('updatedSettings');
696 + $newSettings = [];
697 + if (is_array($rawSettings)) {
698 + foreach ($rawSettings as $key => $value) {
699 + $sanitizedKey = sanitize_text_field($key);
700 + // Value could be string, boolean, or number - sanitize appropriately
701 + if (is_string($value)) {
702 + $sanitizedValue = sanitize_text_field($value);
703 + } elseif (is_bool($value) || is_numeric($value)) {
704 + $sanitizedValue = $value;
705 + } else {
706 + $sanitizedValue = sanitize_text_field((string)$value);
707 + }
708 + $newSettings[$sanitizedKey] = $sanitizedValue;
709 + }
710 + }
711 + $view = $request->getSafe('view', 'sanitize_text_field');
556 712
557 - $this->optionService->updateDashboardViewSettings($newSettings);
713 + $this->optionService->updateDashboardViewSettings($newSettings, $view);
558 714
559 - return $this->sendSuccess([
560 - 'message' => __("Dashboard view settings are updated", 'fluent-boards'),
561 - ], 201);
562 - } catch (\Exception $e) {
563 - return $this->sendError($e->getMessage(), 404);
715 + if ($view == 'listview') {
716 + $message = __("List view settings updated successfully", 'fluent-boards');
717 + } elseif ($view == 'tableview') {
718 + $message = __("Table view settings updated successfully", 'fluent-boards');
719 + } else {
720 + $message = __("Card view settings updated successfully", 'fluent-boards');
564 721 }
722 +
723 + return $this->sendSuccess([
724 + 'message' => $message,
725 + ], 201);
565 726 }
566 727
567 728
568 729 public function getAddonsSettings()
569 730 {
731 + $canAutoInstallKit = $this->canAutoInstallFluentKit();
732 + $kitPluginFile = 'fluent-toolkit/fluent-toolkit.php';
733 + $kitLoaded = defined('FLUENT_TOOLKIT_VERSION');
734 + $kitPluginExists = $this->isPluginInstalled($kitPluginFile);
735 + $kitActionText = __('Get FluentHub from GitHub', 'fluent-boards');
736 +
737 + if ($canAutoInstallKit) {
738 + $kitActionText = $kitPluginExists ? __('Activate FluentHub', 'fluent-boards') : __('Install FluentHub', 'fluent-boards');
739 + }
740 +
570 741 $addOns = [
571 742 'fluent-crm' => [
572 743 'title' => __('FluentCRM', 'fluent-boards'),
573 744 'logo' => fluent_boards_mix('images/addons/fluent-crm.svg'),
@@ -574,9 +745,10 @@
574 745 'is_installed' => defined('FLUENTCRM'),
575 746 'learn_more_url' => 'https://fluentcrm.com/',
576 747 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluentcrm/',
577 748 'action_text' => $this->isPluginInstalled('fluent-crm/fluent-crm.php') ? __('Activate FluentCRM', 'fluent-boards') : __('Install FluentCRM', 'fluent-boards'),
578 - 'description' => __('FluentCRM is a Self Hosted Email Marketing Automation Plugin for WordPress. Manage your leads and customers, email campaigns, automated email sequencing and many more', 'fluent-boards')
749 + 'description' => __('FluentCRM is a Self Hosted Email Marketing Automation Plugin for WordPress. Manage your leads and customers, email campaigns, automated email sequencing and many more', 'fluent-boards'),
750 + 'short_desc' => __('Email marketing automation', 'fluent-boards')
579 751 ],
580 752 'fluentform' => [
581 753 'title' => __('Fluent Forms', 'fluent-boards'),
582 754 'logo' => fluent_boards_mix('images/addons/fluentform.png'),
@@ -583,9 +755,10 @@
583 755 'is_installed' => defined('FLUENTFORM'),
584 756 'learn_more_url' => 'https://wordpress.org/plugins/fluentform/',
585 757 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluent-forms/',
586 758 'action_text' => $this->isPluginInstalled('fluent-form/fluent-form.php') ? __('Activate Fluent Forms', 'fluent-boards') : __('Install Fluent Forms', 'fluent-boards'),
587 - 'description' => __('Collect leads and build any type of forms, accept payments, connect with your CRM with the Fastest Contact Form Builder Plugin for WordPress', 'fluent-boards')
759 + 'description' => __('Collect leads and build any type of forms, accept payments, connect with your CRM with the Fastest Contact Form Builder Plugin for WordPress', 'fluent-boards'),
760 + 'short_desc' => __('Create forms and accept payments', 'fluent-boards')
588 761 ],
589 762 'fluent-support' => [
590 763 'title' => __('Fluent Support', 'fluent-boards'),
591 764 'logo' => fluent_boards_mix('images/addons/fluent-support.svg'),
@@ -593,9 +766,10 @@
593 766 'learn_more_url' => 'https://wordpress.org/plugins/fluent-connect/',
594 767 'settings_url' => admin_url('admin.php?page=fluent-support#/'),
595 768 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluentsupport/',
596 769 'action_text' => $this->isPluginInstalled('fluent-support/fluent-support.php') ? __('Activate Fluent Support', 'fluent-boards') : __('Install Fluent Support', 'fluent-boards'),
597 - 'description' => __('WordPress Helpdesk and Customer Support Ticket Plugin. Provide awesome support and manage customer queries right from your WordPress dashboard.', 'fluent-boards')
770 + 'description' => __('WordPress Helpdesk and Customer Support Ticket Plugin. Provide awesome support and manage customer queries right from your WordPress dashboard.', 'fluent-boards'),
771 + 'short_desc' => __('Customer support ticketing', 'fluent-boards')
598 772 ],
599 773 'fluent-smtp' => [
600 774 'title' => __('Fluent SMTP', 'fluent-boards'),
601 775 'logo' => fluent_boards_mix('images/addons/fluent-smtp.svg'),
@@ -602,10 +776,24 @@
602 776 'is_installed' => defined('FLUENTMAIL'),
603 777 'learn_more_url' => 'https://wordpress.org/plugins/fluent-smtp/',
604 778 'associate_doc' => admin_url('options-general.php?page=fluent-mail#/'),
605 779 'action_text' => $this->isPluginInstalled('fluent-smtp/fluent-smtp.php') ? __('Activate Fluent SMTP', 'fluent-boards') : __('Install Fluent SMTP', 'fluent-boards'),
606 - 'description' => __('The Ultimate SMTP and SES Plugin for WordPress. Connect with any SMTP, SendGrid, Mailgun, SES, Sendinblue, PepiPost, Google, Microsoft and more.', 'fluent-boards')
780 + 'description' => __('The Ultimate SMTP and SES Plugin for WordPress. Connect with any SMTP, SendGrid, Mailgun, SES, Sendinblue, PepiPost, Google, Microsoft and more.', 'fluent-boards'),
781 + 'short_desc' => __('Reliable email delivery with SMTP', 'fluent-boards')
607 782 ],
783 + 'fluent-toolkit' => [
784 + 'title' => __('FluentHub', 'fluent-boards'),
785 + 'logo' => fluent_boards_mix('images/addons/fluent-toolkit.svg'),
786 + 'is_installed' => $kitLoaded,
787 + 'learn_more_url' => 'https://github.com/WPManageNinja/fluent-toolkit',
788 + 'settings_url' => admin_url('admin.php?page=fluent-toolkit'),
789 + 'associate_doc' => 'https://github.com/WPManageNinja/fluent-toolkit',
790 + 'action_text' => $kitActionText,
791 + 'install_route' => $canAutoInstallKit ? 'admin/mcp/install-adapter' : '',
792 + 'install_url' => $canAutoInstallKit ? '' : 'https://github.com/WPManageNinja/fluent-toolkit',
793 + 'description' => __('Fluent Boards MCP tools become available after FluentHub is installed and active.', 'fluent-boards'),
794 + 'short_desc' => __('AI agent tools for Fluent Boards', 'fluent-boards')
795 + ],
608 796 ];
609 797
610 798 $addOns = apply_filters('fluent_boards/addons_settings', $addOns);
611 799
@@ -630,16 +818,27 @@
630 818 'message' => __('This feature is only available in Fluent Boards Pro', 'fluent-boards')
631 819 ]);
632 820 }
633 821
634 - $settings = $request->get('settings', []);
822 + $rawSettings = $request->get('settings', []);
823 +
824 + // Validate that settings is an array
825 + if (!is_array($rawSettings)) {
826 + return $this->sendError([
827 + 'message' => __('Invalid settings format', 'fluent-boards')
828 + ], 400);
829 + }
635 830
636 831 $prefSettings = fluent_boards_get_pref_settings(false);
637 832
638 - $settings = wp_parse_args($settings, $prefSettings);
833 + $settings = wp_parse_args($rawSettings, $prefSettings);
639 834
640 835 $settings = Arr::only($settings, array_keys($prefSettings));
641 - $settings['frontend']['slug'] = sanitize_title($settings['frontend']['slug']);
836 +
837 + // Sanitize slug if it exists
838 + if (isset($settings['frontend']['slug'])) {
839 + $settings['frontend']['slug'] = sanitize_title($settings['frontend']['slug']);
840 + }
642 841
643 842 if (empty($settings['frontend']['slug'])) {
644 843 $settings['frontend']['slug'] = 'projects';
645 844 }
@@ -651,8 +850,12 @@
651 850 do_action('fluent_boards/saving_addons', $settings, $prefSettings);
652 851
653 852 update_option('fluent_boards_modules', $settings, 'yes');
654 853
854 + if (isset($settings['recurring_task']['enabled']) && $settings['recurring_task']['enabled'] == 'no') {
855 + do_action('fluent_boards/recurring_task_disabled');
856 + }
857 +
655 858 return $this->sendSuccess([
656 859 'message' => __('Settings are saved', 'fluent-boards'),
657 860 'featureModules' => $settings
658 861 ]);
@@ -661,9 +864,9 @@
661 864 public function installPlugin(Request $request)
662 865 {
663 866 if (!current_user_can('install_plugins')) {
664 867 return $this->sendError([
665 - 'message' => __('Sorry! you do not have permission to install plugin', 'fluent-crm')
868 + 'message' => __('Sorry! you do not have permission to install plugin', 'fluent-boards')
666 869 ]);
667 870 }
668 871
669 872 $plugin = $request->getSafe('plugin', 'sanitize_text_field');
@@ -705,8 +908,19 @@
705 908 {
706 909 return file_exists(WP_PLUGIN_DIR . '/' . $plugin);
707 910 }
708 911
912 + private function canAutoInstallFluentKit()
913 + {
914 + $canAutoInstall = (bool) apply_filters('fluent_kit/can_auto_install', false);
915 +
916 + if (!$canAutoInstall) {
917 + $canAutoInstall = (bool) apply_filters('fluent_toolkit/can_auto_install', false);
918 + }
919 +
920 + return $canAutoInstall;
921 + }
922 +
709 923 private function backgroundInstaller($plugin_to_install, $plugin_id)
710 924 {
711 925 if (!empty($plugin_to_install['repo-slug'])) {
712 926 require_once ABSPATH . 'wp-admin/includes/file.php';
@@ -758,9 +972,9 @@
758 972 )
759 973 );
760 974
761 975 if (is_wp_error($plugin_information)) {
762 - throw new \Exception($plugin_information->get_error_message());
976 + throw new \Exception(esc_html($plugin_information->get_error_message()));
763 977 }
764 978
765 979 $package = $plugin_information->download_link;
766 980 $download = $upgrader->download_package($package);
@@ -765,15 +979,15 @@
765 979 $package = $plugin_information->download_link;
766 980 $download = $upgrader->download_package($package);
767 981
768 982 if (is_wp_error($download)) {
769 - throw new \Exception($download->get_error_message());
983 + throw new \Exception(esc_html($download->get_error_message()));
770 984 }
771 985
772 986 $working_dir = $upgrader->unpack_package($download, true);
773 987
774 988 if (is_wp_error($working_dir)) {
775 - throw new \Exception($working_dir->get_error_message());
989 + throw new \Exception(esc_html($working_dir->get_error_message()));
776 990 }
777 991
778 992 $result = $upgrader->install_package(
779 993 array(
@@ -789,9 +1003,9 @@
789 1003 )
790 1004 );
791 1005
792 1006 if (is_wp_error($result)) {
793 - throw new \Exception($result->get_error_message());
1007 + throw new \Exception(esc_html($result->get_error_message()));
794 1008 }
795 1009
796 1010 $activate = true;
797 1011
@@ -809,9 +1023,9 @@
809 1023 try {
810 1024 $result = activate_plugin($installed ? $installed_plugins[$plugin_file] : $plugin_slug . '/' . $plugin_file);
811 1025
812 1026 if (is_wp_error($result)) {
813 - throw new \Exception($result->get_error_message());
1027 + throw new \Exception(esc_html($result->get_error_message()));
814 1028 }
815 1029 } catch (\Exception $e) {
816 1030 }
817 1031 }
@@ -852,9 +1066,10 @@
852 1066 $pages = [];
853 1067 foreach ($allPages as $page) {
854 1068 $pages[] = [
855 1069 'id' => $page->ID,
856 - 'title' => $page->post_title ? $page->post_title : __('(no title)', 'fluent-boards')
1070 + 'title' => $page->post_title ? $page->post_title : __('(no title)', 'fluent-boards'),
1071 + 'url' => esc_url_raw(get_permalink($page->ID))
857 1072 ];
858 1073 }
859 1074
860 1075 return $this->sendSuccess([
@@ -880,9 +1095,25 @@
880 1095 return $this->sendError([
881 1096 'message' => __('This feature is only available in Fluent Boards Pro. Please upgrade.', 'fluent-boards')
882 1097 ]);
883 1098 }
884 - $settings = $request->getSafe('updatedSettings', []);
1099 + // updatedSettings is an array, sanitize each element
1100 + $rawSettings = $request->get('updatedSettings', []);
1101 + $settings = [];
1102 + if (is_array($rawSettings)) {
1103 + foreach ($rawSettings as $key => $value) {
1104 + $sanitizedKey = sanitize_text_field($key);
1105 + // Value could be string, boolean, or number - sanitize appropriately
1106 + if (is_string($value)) {
1107 + $sanitizedValue = sanitize_text_field($value);
1108 + } elseif (is_bool($value) || is_numeric($value)) {
1109 + $sanitizedValue = $value;
1110 + } else {
1111 + $sanitizedValue = sanitize_text_field((string)$value);
1112 + }
1113 + $settings[$sanitizedKey] = $sanitizedValue;
1114 + }
1115 + }
885 1116
886 1117 $settings = apply_filters('fluent_boards/save_general_settings', $settings);
887 1118
888 1119 $savedSettings = fluent_boards_update_option('general_settings', $settings);
@@ -889,12 +1120,14 @@
889 1120 $savedGeneralSettings = \maybe_unserialize($savedSettings->value);
890 1121
891 1122 $scheduleHandler = new ProScheduleHandler();
892 1123
893 - if ($savedGeneralSettings['daily_reminder_enabled'] || $savedGeneralSettings['daily_reminder_enabled'] == 'true') {
1124 + $dailyReminderEnabled = $savedGeneralSettings['daily_reminder_enabled'] ?? false;
1125 +
1126 + if (filter_var($dailyReminderEnabled, FILTER_VALIDATE_BOOLEAN)) {
894 1127 // force schedule from this settings update
895 - $scheduleHandler->clearSchedule();
896 - $scheduleHandler->schedule();
1128 + $scheduleHandler->clearDailyTaskReminderScheduler();
1129 + $scheduleHandler->scheduleDailyTaskReminder();
897 1130 }
898 1131
899 1132 return $this->sendSuccess([
900 1133 'settings' => $savedGeneralSettings,