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 +413 -100 1.11trunk View file →
@@ -8,8 +8,9 @@
8 8 use FluentBoards\App\Models\User;
9 9 use FluentBoards\App\Models\Board;
10 10 use FluentBoards\App\Models\Relation;
11 11 use FluentBoards\App\Services\BoardService;
12 +use FluentBoards\App\Services\DescriptionMarkdownConverter;
12 13 use FluentBoards\App\Services\Helper;
13 14 use FluentBoards\App\Services\OptionService;
14 15 use FluentBoards\App\Services\Constant;
15 16 use FluentBoards\App\Services\PermissionManager;
@@ -14,8 +15,9 @@
14 15 use FluentBoards\App\Services\Constant;
15 16 use FluentBoards\App\Services\PermissionManager;
16 17 use FluentBoards\Framework\Http\Request\Request;
17 18 use FluentBoards\Framework\Support\Arr;
19 +use FluentBoardsPro\App\Hooks\Handlers\ProScheduleHandler;
18 20
19 21 class OptionsController extends Controller
20 22 {
21 23 private $boardService;
@@ -30,37 +32,55 @@
30 32
31 33 public function selectorOptions(Request $request)
32 34 {
33 35 try {
34 - $optionKey = $request->getSafe('option_key');
35 - $search = $request->getSafe('search');
36 + $optionKey = $request->getSafe('option_key', 'sanitize_text_field');
37 + $search = $request->getSafe('search', 'sanitize_text_field');
36 38 $includedIds = $request->getSafe('values');
37 - $boardId = $request->getSafe('board_id');
39 + $boardId = $request->getSafe('board_id', 'intval');
38 40
39 41 $options = [];
40 42 if ('users' === $optionKey || 'task_assignees' === $optionKey) { // no ajax/code is designed to handle this eventually will goto else
41 43
42 44 if (!PermissionManager::isBoardManager($boardId)) {
43 - throw new \Exception('You do not have permission to access this route');
45 + throw new \Exception(esc_html__('You do not have permission to access this route', 'fluent-boards'));
44 46 }
45 47
46 48 if (!defined('FLUENT_BOARDS_PRO')) {
47 49 // get who has 'manage_options' capability
48 - $users = PermissionManager::getAll_WP_Admins();
50 + $users = PermissionManager::getAll_WP_Admins($search);
49 51
50 52 } else {
51 - $users = User::query()
52 - ->when($search, function ($query) use ($search) {
53 - return $query->where('display_name', 'LIKE', '%' . $search . '%')->orWhere('user_email', 'LIKE', '%' . $search . '%');
54 - })
55 - ->limit(20)->get();
53 + // Search by user login, email, and nicename, first_name , last_name
54 + $users = Helper::searchWordPressUsers($search);
55 + $users = Helper::sanitizeUsersArray($users, $boardId);
56 +
56 57 }
57 58
58 59 $options = $this->addUserDataAsSelectorOption($users);
59 60
61 + } elseif ('board_create_users' === $optionKey) {
62 + if (!PermissionManager::userHasBoardCreationPermission()) {
63 + throw new \Exception(esc_html__('You do not have permission to access this route', 'fluent-boards'));
64 + }
65 +
66 + if (!defined('FLUENT_BOARDS_PRO')) {
67 + // Bound the lookup: this popover searches on focus (empty query too).
68 + $users = PermissionManager::getAll_WP_Admins($search, 20);
69 + } else {
70 + $users = Helper::searchWordPressUsers($search);
71 + }
72 +
73 + $options = $this->addUserDataAsSelectorOption($users);
74 + // Board creation can be delegated to members without `list_users`;
75 + // don't leak full account emails to them.
76 + $options = $this->maskSelectorEmailsForViewer($options);
77 +
60 78 } elseif ('boards' === $optionKey) {
61 79 $boards = Board::query()
80 + ->byAccessUser(get_current_user_id())
62 81 ->when($search, function ($query) use ($search) {
82 + // $search is already sanitized with sanitize_text_field above
63 83 return $query->where('title', 'LIKE', '%' . $search . '%');
64 84 })->take(20)->get();
65 85
66 86 foreach ($boards as $board) {
@@ -70,9 +90,36 @@
70 90 'left_side_value' => $board->title,
71 91 'right_side_value' => $board->slug,
72 92 ];
73 93 }
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
100 + $tasks = Task::where('board_id', $boardId)
101 + ->whereNull('archived_at')
102 + ->whereNull('parent_id')
103 + ->when($search, function ($query) use ($search) {
104 + // $search is already sanitized with sanitize_text_field above
105 + return $query->where('title', 'LIKE', '%' . $search . '%');
106 + })->take(20)->get();
107 +
108 + foreach ($tasks as $task) {
109 + $options[] = [
110 + 'id' => $task->id,
111 + 'title' => $task->title,
112 + 'board_id' => $task->board_id,
113 + 'subtask_groups' => $task->subtaskGroup
114 + ];
115 + }
116 +
74 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 +
75 122 $users = (new BoardService())->getAssigneesByBoard($boardId, $search);
76 123 $options = $this->addUserDataAsSelectorOption($users);
77 124 } else {
78 125 $options = apply_filters('fluent_boards/ajax_options_' . $optionKey, [], $search, $includedIds);
@@ -91,17 +138,51 @@
91 138 $options = [];
92 139 foreach ($users as $user) {
93 140 $options[] = [
94 141 'id' => $user->ID,
142 + 'email' => $user->user_email,
143 + 'name' => $user->display_name ?? $user->user_email,
95 144 'title' => $user->display_name . ' (' . $user->user_email . ')',
96 145 'photo' => get_avatar_url($user->user_email),
97 - 'left_side_value' => $user->display_name,
98 - 'right_side_value' => $user->user_email,
99 146 ];
100 147 }
101 148 return $options;
102 149 }
103 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 +
104 185 public function getCurrentUserPermissions()
105 186 {
106 187 try {
107 188 $currentUserBoards = Relation::query()
@@ -123,10 +204,10 @@
123 204
124 205 public function getUserPermission(Request $request)
125 206 {
126 207 try {
127 - $boardId = $request->getSafe('boardId');
128 - $userId = $request->getSafe('userId');
208 + $boardId = $request->getSafe('boardId', 'intval');
209 + $userId = $request->getSafe('userId', 'intval');
129 210
130 211 $boardUser = Relation::where('board_id', $boardId)
131 212 ->where('user_id', $userId)
132 213 ->where('status', 'ACTIVE')->first();
@@ -144,12 +225,12 @@
144 225
145 226 public function updatedUserPermission(Request $request)
146 227 {
147 228 try {
148 - $permission = $request->getSafe('userPermission');
149 - $updateType = $request->getSafe('updateType');
150 - $boardId = $request->getSafe('boardId');
151 - $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');
152 233
153 234 $boardUser = Relation::where('board_id', $boardId)->where('user_id', $userId)->status('ACTIVE')->first();
154 235
155 236 if ('Board Admin' == $permission) {
@@ -181,8 +262,9 @@
181 262 }
182 263
183 264 public function SetUserSuperAdmin($userId)
184 265 {
266 + $userId = absint($userId);
185 267 try {
186 268 $this->optionService->createSuperAdmin($userId);
187 269 return $this->sendSuccess([
188 270 'message' => __('Member has been set super admin successfully!', 'fluent-boards')
@@ -194,8 +276,9 @@
194 276 }
195 277
196 278 public function removeUserSuperAdmin($userId)
197 279 {
280 + $userId = absint($userId);
198 281 try {
199 282 $this->optionService->removeUserSuperAdmin($userId);
200 283
201 284 return $this->sendSuccess([
@@ -208,9 +291,9 @@
208 291
209 292 public function IsUserAllBoardAdmin(Request $request)
210 293 {
211 294 try {
212 - $userId = $request->getSafe('id');
295 + $userId = $request->getSafe('id', 'intval');
213 296 $isSuperAdmin = false;
214 297 $superAdmin = Relation::where('board_id', null)->where('user_id', $userId)->where('status', 'ACTIVE')->first();
215 298 $totalSuperAdmin = Relation::where('board_id', null)->where('status', 'ACTIVE')->count();
216 299 $permissions = [];
@@ -230,10 +313,11 @@
230 313 }
231 314
232 315 public function RemoveUserFromSuperAdmin(Request $request, $id)
233 316 {
317 + $id = absint($id);
234 318 try {
235 - $userId = $request->getSafe('id');
319 + $userId = $request->getSafe('id', 'intval');
236 320
237 321 $superAdmin = Relation::where('board_id', null)->where('user_id', $userId)->first();
238 322 $superAdmin->status = 'INACTIVE';
239 323 $superAdmin->save();
@@ -248,10 +332,10 @@
248 332
249 333 public function removeUserFromBoard(Request $request)
250 334 {
251 335 try {
252 - $boardId = $request->getSafe('boardId');
253 - $userId = $request->getSafe('userId');
336 + $boardId = $request->getSafe('boardId', 'intval');
337 + $userId = $request->getSafe('userId', 'intval');
254 338
255 339 $this->boardService->removeUserFromBoard($boardId, $userId);
256 340
257 341 if (!PermissionManager::isAdmin($userId)) {
@@ -268,9 +352,14 @@
268 352
269 353 public function addAsSuperAdmin(Request $request)
270 354 {
271 355 try {
272 - $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 + }
273 362 foreach ($userIds as $userId) {
274 363 $this->createSuperAdmin($userId);
275 364 }
276 365
@@ -299,10 +388,20 @@
299 388
300 389 public function addMembersInBoards(Request $request)
301 390 {
302 391 try {
303 - $userIds = $request->getSafe('memberIds');
304 - $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 + }
305 404
306 405 foreach ($userIds as $userId) {
307 406 foreach ($boardIds as $boardId) {
308 407 $this->boardService->addMembersInBoard($boardId, $userId);
@@ -320,9 +419,18 @@
320 419
321 420 public function updateGlobalNotificationSettings(Request $request)
322 421 {
323 422 try {
324 - $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 + }
325 433
326 434 $this->optionService->updateGlobalNotificationSettings($newSettings);
327 435
328 436 return $this->sendSuccess([
@@ -357,21 +465,29 @@
357 465 }
358 466
359 467 $boardId = $request->getSafe('boardId', 'intval');
360 468
361 - $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)
362 476 ->select(['foreign_id'])
363 477 ->groupBy('foreign_id');
364 478
365 479 if ($boardId) {
366 - $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);
367 484 }
368 485
369 - $members = [];
370 -
371 - $memberUserIds = $memberUserIds->get()
486 + $memberUserIds = $memberUserIdsQuery->get()
372 487 ->pluck('foreign_id')->toArray();
373 488
489 + $members = [];
374 490
375 491 if ($memberUserIds) {
376 492 $memberUsers = get_users([
377 493 'include' => $memberUserIds
@@ -421,70 +537,106 @@
421 537 'members' => $members
422 538 ];
423 539 }
424 540
425 - public function quickSearch()
541 + public function globalSearch()
426 542 {
427 543 $currentUserId = get_current_user_id();
428 544
429 - $query = sanitize_text_field($_REQUEST['query']);
430 - $query = strtolower($query);
431 - $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';
432 549
433 - $tasksQuery = Task::query()->where('parent_id', null)->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']);
434 - $isUserAdmin = PermissionManager::isAdmin($currentUserId);
550 + // Pagination parameters
551 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
552 + $taskPage = isset($_REQUEST['task_page']) ? max(1, (int)$_REQUEST['task_page']) : 0;
553 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
554 + $boardPage = isset($_REQUEST['board_page']) ? max(1, (int)$_REQUEST['board_page']) : 0;
555 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
556 + $perPage = isset($_REQUEST['per_page']) ? (int)$_REQUEST['per_page'] : 20;
557 + $perPage = max(1, min(100, $perPage));
435 558
436 - // This is a check for deleted tasks to be excluded from search results
437 - $allActiveBoardsIds = Board::query()->where('archived_at', null)->pluck('id')->toArray();
559 + // Build base queries
560 + $firstThreeChars = substr($query, 0, 3);
561 + $firstNineChars = substr($query, 0, 9);
438 562
563 + if($firstThreeChars == 'id:') {
564 + $idPart = preg_replace('/[^a-zA-Z0-9]/', '', substr($query, 3));
565 + $tasksQuery = Task::query()->where('parent_id', null)->whereRaw('id LIKE ?', ['%' . $idPart . '%']);
566 + $boardQuery = Board::query()->whereRaw('id LIKE ?', ['%' . $idPart . '%']);
567 + }elseif($firstNineChars == 'archived:') {
568 + $archivedPart = trim(substr($query, 9));
569 + $tasksQuery = Task::query()->where('parent_id', null)->whereNotNull('archived_at')->whereRaw('LOWER(title) LIKE ?', ['%' . $archivedPart . '%']);
570 + $boardQuery = Board::query()->whereNotNull('archived_at')->whereRaw('LOWER(title) LIKE ?', ['%' . $archivedPart . '%']);
571 + } else {
572 + $tasksQuery = Task::query()->where('parent_id', null)->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']);
573 + $boardQuery = Board::query()->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']);
574 + }
575 +
576 + // Apply scope and permissions
577 + $isUserAdmin = PermissionManager::isAdmin($currentUserId);
439 578 if ($scope == 'all') {
440 - $boardQuery = Board::query()->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']);
441 - if ($isUserAdmin) {
442 - $boards = $boardQuery->get();
443 - $tasks = $tasksQuery->get();
444 - } else {
579 + if (!$isUserAdmin) {
445 580 $boardIds = PermissionManager::getBoardIdsForUser($currentUserId);
446 - $boards = $boardQuery->whereIn('id', $boardIds)->get();
447 - $tasks = $tasksQuery->whereIn('board_id', $boardIds)->get();
581 + $boardQuery->whereIn('id', $boardIds);
582 + $tasksQuery->whereIn('board_id', $boardIds);
448 583 }
449 584 } else {
450 - $boards = []; // boards results is not needed in scoped search
451 - $inBoard = (int)$scope;
452 - if ($isUserAdmin || in_array($inBoard, $boardIds = PermissionManager::getBoardIdsForUser($currentUserId))) {
453 - $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);
454 591 } else {
455 - // Out of permission scope search
456 - $tasks = [];
592 + $tasksQuery->where('id', -1); // Force no results
457 593 }
458 594 }
459 595
596 + $allActiveBoardsIds = Board::query()->where('archived_at', null)->pluck('id')->toArray();
460 597
598 + $boards = [];
599 + $tasks = [];
600 + $totalBoards = 0;
601 + $totalTasks = 0;
602 + $formattedBoards = [];
461 603 $formattedTasks = [];
462 - $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 +
463 619 foreach ($boards as $board) {
464 620 $formattedBoards[] = [
465 621 'type' => 'board',
466 622 'id' => $board->id,
467 623 'title' => $board->title,
468 - 'description' => $board->description,
469 - 'url' => Helper::getBoardUrl($board->id)
624 + 'description' => DescriptionMarkdownConverter::normalize($board->description),
470 625 ];
471 626 }
472 627 foreach ($tasks as $task) {
473 -
474 628 if (!in_array($task->board_id, $allActiveBoardsIds)) {
475 - // if the task is not in an active board, skip it
476 629 continue;
477 630 }
478 631
479 632 $board = $task->board;
480 -
481 633 $formattedTasks[] = [
482 634 'type' => 'task',
483 635 'id' => $task->id,
484 636 'title' => $task->title,
485 - 'description' => $task->description,
486 - 'url' => Helper::getTaskUrl($task->id, $board->id),
637 + 'description' => DescriptionMarkdownConverter::normalize($task->description),
638 + 'board_id' => $task->board_id,
487 639 'board' => [
488 640 'id' => $board->id,
489 641 'title' => $board->title,
490 642 'url' => Helper::getBoardUrl($board->id)
@@ -492,51 +644,101 @@
492 644 'stage' => [
493 645 'id' => $task->stage_id,
494 646 'title' => $task->stage->title ?? '',
495 647 ],
496 -
497 648 ];
498 649 }
650 +
499 651 return $this->sendSuccess([
500 - 'tasks' => $formattedTasks,
501 - '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 + ]
502 666 ], 200);
503 -
504 667 }
505 668
506 - public function getDashboardViewSettings()
669 + public function getDashboardViewSettings(Request $request)
507 670 {
508 - try {
671 + $view = $request->getSafe('view', 'sanitize_text_field');
672 +
673 + if ($view == 'kanbanview') {
509 674 $globalSettings = $this->optionService->getDashboardViewSettings();
510 - if ($globalSettings->value)
511 - $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 + }
512 683
513 - return $this->sendSuccess([
514 - 'currentSettings' => $currentSettings,
515 - ], 200);
516 - } catch (\Exception $e) {
517 - return $this->sendError($e->getMessage(), 404);
518 - }
684 + if ($globalSettings->value)
685 + $currentSettings = maybe_unserialize($globalSettings->value);
686 +
687 + return $this->sendSuccess([
688 + 'currentSettings' => $currentSettings,
689 + ], 200);
519 690 }
520 691
521 692 public function updateDashboardViewSettings(Request $request)
522 693 {
523 - try {
524 - $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');
525 712
526 - $this->optionService->updateDashboardViewSettings($newSettings);
713 + $this->optionService->updateDashboardViewSettings($newSettings, $view);
527 714
528 - return $this->sendSuccess([
529 - 'message' => __("Dashboard view settings are updated", 'fluent-boards'),
530 - ], 201);
531 - } catch (\Exception $e) {
532 - 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');
533 721 }
722 +
723 + return $this->sendSuccess([
724 + 'message' => $message,
725 + ], 201);
534 726 }
535 727
536 728
537 729 public function getAddonsSettings()
538 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 +
539 741 $addOns = [
540 742 'fluent-crm' => [
541 743 'title' => __('FluentCRM', 'fluent-boards'),
542 744 'logo' => fluent_boards_mix('images/addons/fluent-crm.svg'),
@@ -543,9 +745,10 @@
543 745 'is_installed' => defined('FLUENTCRM'),
544 746 'learn_more_url' => 'https://fluentcrm.com/',
545 747 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluentcrm/',
546 748 'action_text' => $this->isPluginInstalled('fluent-crm/fluent-crm.php') ? __('Activate FluentCRM', 'fluent-boards') : __('Install FluentCRM', 'fluent-boards'),
547 - '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')
548 751 ],
549 752 'fluentform' => [
550 753 'title' => __('Fluent Forms', 'fluent-boards'),
551 754 'logo' => fluent_boards_mix('images/addons/fluentform.png'),
@@ -552,9 +755,10 @@
552 755 'is_installed' => defined('FLUENTFORM'),
553 756 'learn_more_url' => 'https://wordpress.org/plugins/fluentform/',
554 757 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluent-forms/',
555 758 'action_text' => $this->isPluginInstalled('fluent-form/fluent-form.php') ? __('Activate Fluent Forms', 'fluent-boards') : __('Install Fluent Forms', 'fluent-boards'),
556 - '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')
557 761 ],
558 762 'fluent-support' => [
559 763 'title' => __('Fluent Support', 'fluent-boards'),
560 764 'logo' => fluent_boards_mix('images/addons/fluent-support.svg'),
@@ -562,9 +766,10 @@
562 766 'learn_more_url' => 'https://wordpress.org/plugins/fluent-connect/',
563 767 'settings_url' => admin_url('admin.php?page=fluent-support#/'),
564 768 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluentsupport/',
565 769 'action_text' => $this->isPluginInstalled('fluent-support/fluent-support.php') ? __('Activate Fluent Support', 'fluent-boards') : __('Install Fluent Support', 'fluent-boards'),
566 - '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')
567 772 ],
568 773 'fluent-smtp' => [
569 774 'title' => __('Fluent SMTP', 'fluent-boards'),
570 775 'logo' => fluent_boards_mix('images/addons/fluent-smtp.svg'),
@@ -571,12 +776,28 @@
571 776 'is_installed' => defined('FLUENTMAIL'),
572 777 'learn_more_url' => 'https://wordpress.org/plugins/fluent-smtp/',
573 778 'associate_doc' => admin_url('options-general.php?page=fluent-mail#/'),
574 779 'action_text' => $this->isPluginInstalled('fluent-smtp/fluent-smtp.php') ? __('Activate Fluent SMTP', 'fluent-boards') : __('Install Fluent SMTP', 'fluent-boards'),
575 - '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')
576 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 + ],
577 796 ];
578 797
798 + $addOns = apply_filters('fluent_boards/addons_settings', $addOns);
799 +
579 800 $modules = fluent_boards_get_pref_settings(false);
580 801
581 802 if (empty($modules['frontend']['render_type'])) {
582 803 $modules['frontend']['render_type'] = 'standalone';
@@ -597,16 +818,27 @@
597 818 'message' => __('This feature is only available in Fluent Boards Pro', 'fluent-boards')
598 819 ]);
599 820 }
600 821
601 - $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 + }
602 830
603 831 $prefSettings = fluent_boards_get_pref_settings(false);
604 832
605 - $settings = wp_parse_args($settings, $prefSettings);
833 + $settings = wp_parse_args($rawSettings, $prefSettings);
606 834
607 835 $settings = Arr::only($settings, array_keys($prefSettings));
608 - $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 + }
609 841
610 842 if (empty($settings['frontend']['slug'])) {
611 843 $settings['frontend']['slug'] = 'projects';
612 844 }
@@ -618,8 +850,12 @@
618 850 do_action('fluent_boards/saving_addons', $settings, $prefSettings);
619 851
620 852 update_option('fluent_boards_modules', $settings, 'yes');
621 853
854 + if (isset($settings['recurring_task']['enabled']) && $settings['recurring_task']['enabled'] == 'no') {
855 + do_action('fluent_boards/recurring_task_disabled');
856 + }
857 +
622 858 return $this->sendSuccess([
623 859 'message' => __('Settings are saved', 'fluent-boards'),
624 860 'featureModules' => $settings
625 861 ]);
@@ -628,15 +864,15 @@
628 864 public function installPlugin(Request $request)
629 865 {
630 866 if (!current_user_can('install_plugins')) {
631 867 return $this->sendError([
632 - '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')
633 869 ]);
634 870 }
635 871
636 872 $plugin = $request->getSafe('plugin', 'sanitize_text_field');
637 873
638 - $acceptedPlugins = [
874 + $acceptedFreePlugins = [
639 875 'fluent-crm' => 'fluent-crm.php',
640 876 'fluentform' => 'fluentform.php',
641 877 'fluent-support' => 'fluent-support.php',
642 878 'fluent-smtp' => 'fluent-smtp.php'
@@ -641,8 +877,10 @@
641 877 'fluent-support' => 'fluent-support.php',
642 878 'fluent-smtp' => 'fluent-smtp.php'
643 879 ];
644 880
881 + $acceptedPlugins = apply_filters('fluent_boards/accepted_plugins', $acceptedFreePlugins);
882 +
645 883 if (!isset($acceptedPlugins[$plugin])) {
646 884 return $this->sendError([
647 885 'message' => __('Invalid plugin', 'fluent-boards')
648 886 ]);
@@ -647,16 +885,20 @@
647 885 'message' => __('Invalid plugin', 'fluent-boards')
648 886 ]);
649 887 }
650 888
651 -
652 889 $pluginToInstall = [
653 - 'name' => __('Fluent Plugin', 'fluent-crm'),
890 + 'name' => __('Fluent Plugin', 'fluent-boards'),
654 891 'repo-slug' => $plugin,
655 892 'file' => $acceptedPlugins[$plugin],
656 893 ];
657 894
658 - $this->backgroundInstaller($pluginToInstall, $plugin);
895 + // if plugin in free list then run background intaller otherwise call an action to install
896 + if (isset($acceptedFreePlugins[$plugin])) {
897 + $this->backgroundInstaller($pluginToInstall, $plugin);
898 + } else {
899 + do_action('fluent_boards/install_plugin', $pluginToInstall, $plugin);
900 + }
659 901
660 902 return $this->sendSuccess([
661 903 'message' => __('Plugin is being installed', 'fluent-boards')
662 904 ]);
@@ -666,8 +908,19 @@
666 908 {
667 909 return file_exists(WP_PLUGIN_DIR . '/' . $plugin);
668 910 }
669 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 +
670 923 private function backgroundInstaller($plugin_to_install, $plugin_id)
671 924 {
672 925 if (!empty($plugin_to_install['repo-slug'])) {
673 926 require_once ABSPATH . 'wp-admin/includes/file.php';
@@ -719,9 +972,9 @@
719 972 )
720 973 );
721 974
722 975 if (is_wp_error($plugin_information)) {
723 - throw new \Exception($plugin_information->get_error_message());
976 + throw new \Exception(esc_html($plugin_information->get_error_message()));
724 977 }
725 978
726 979 $package = $plugin_information->download_link;
727 980 $download = $upgrader->download_package($package);
@@ -726,15 +979,15 @@
726 979 $package = $plugin_information->download_link;
727 980 $download = $upgrader->download_package($package);
728 981
729 982 if (is_wp_error($download)) {
730 - throw new \Exception($download->get_error_message());
983 + throw new \Exception(esc_html($download->get_error_message()));
731 984 }
732 985
733 986 $working_dir = $upgrader->unpack_package($download, true);
734 987
735 988 if (is_wp_error($working_dir)) {
736 - throw new \Exception($working_dir->get_error_message());
989 + throw new \Exception(esc_html($working_dir->get_error_message()));
737 990 }
738 991
739 992 $result = $upgrader->install_package(
740 993 array(
@@ -750,9 +1003,9 @@
750 1003 )
751 1004 );
752 1005
753 1006 if (is_wp_error($result)) {
754 - throw new \Exception($result->get_error_message());
1007 + throw new \Exception(esc_html($result->get_error_message()));
755 1008 }
756 1009
757 1010 $activate = true;
758 1011
@@ -770,9 +1023,9 @@
770 1023 try {
771 1024 $result = activate_plugin($installed ? $installed_plugins[$plugin_file] : $plugin_slug . '/' . $plugin_file);
772 1025
773 1026 if (is_wp_error($result)) {
774 - throw new \Exception($result->get_error_message());
1027 + throw new \Exception(esc_html($result->get_error_message()));
775 1028 }
776 1029 } catch (\Exception $e) {
777 1030 }
778 1031 }
@@ -813,9 +1066,10 @@
813 1066 $pages = [];
814 1067 foreach ($allPages as $page) {
815 1068 $pages[] = [
816 1069 'id' => $page->ID,
817 - '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))
818 1072 ];
819 1073 }
820 1074
821 1075 return $this->sendSuccess([
@@ -820,7 +1074,66 @@
820 1074
821 1075 return $this->sendSuccess([
822 1076 'pages' => $pages
823 1077 ]);
1078 + }
1079 +
1080 + public function getGeneralSettings()
1081 + {
1082 + $settings = fluent_boards_get_option('general_settings', []);
1083 +
1084 + return $this->sendSuccess([
1085 + 'settings' => $settings,
1086 + 'server_timezone' => \wp_timezone_string()
1087 + ]);
1088 +
1089 + }
1090 +
1091 + public function saveGeneralSettings(Request $request)
1092 + {
1093 + // check for pro version
1094 + if (!defined('FLUENT_BOARDS_PRO')) {
1095 + return $this->sendError([
1096 + 'message' => __('This feature is only available in Fluent Boards Pro. Please upgrade.', 'fluent-boards')
1097 + ]);
1098 + }
1099 + // updatedSettings is an array, sanitize each element
1100 + $rawSettings = $request->get('updatedSettings', []);
1101 + $settings = [];
1102 + if (is_array($rawSettings)) {
1103 + foreach ($rawSettings as $key => $value) {
1104 + $sanitizedKey = sanitize_text_field($key);
1105 + // Value could be string, boolean, or number - sanitize appropriately
1106 + if (is_string($value)) {
1107 + $sanitizedValue = sanitize_text_field($value);
1108 + } elseif (is_bool($value) || is_numeric($value)) {
1109 + $sanitizedValue = $value;
1110 + } else {
1111 + $sanitizedValue = sanitize_text_field((string)$value);
1112 + }
1113 + $settings[$sanitizedKey] = $sanitizedValue;
1114 + }
1115 + }
1116 +
1117 + $settings = apply_filters('fluent_boards/save_general_settings', $settings);
1118 +
1119 + $savedSettings = fluent_boards_update_option('general_settings', $settings);
1120 + $savedGeneralSettings = \maybe_unserialize($savedSettings->value);
1121 +
1122 + $scheduleHandler = new ProScheduleHandler();
1123 +
1124 + $dailyReminderEnabled = $savedGeneralSettings['daily_reminder_enabled'] ?? false;
1125 +
1126 + if (filter_var($dailyReminderEnabled, FILTER_VALIDATE_BOOLEAN)) {
1127 + // force schedule from this settings update
1128 + $scheduleHandler->clearDailyTaskReminderScheduler();
1129 + $scheduleHandler->scheduleDailyTaskReminder();
1130 + }
1131 +
1132 + return $this->sendSuccess([
1133 + 'settings' => $savedGeneralSettings,
1134 + 'message' => __('Settings are saved', 'fluent-boards')
1135 + ]);
1136 +
824 1137 }
825 1138
826 1139 }