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 +429 -101 1.11 → 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;
@@ -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,70 @@
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);
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) {
@@ -70,11 +105,38 @@
70 105 'left_side_value' => $board->title,
71 106 'right_side_value' => $board->slug,
72 107 ];
73 108 }
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
115 + $tasks = Task::where('board_id', $boardId)
116 + ->whereNull('archived_at')
117 + ->whereNull('parent_id')
118 + ->when($search, function ($query) use ($search) {
119 + // $search is already sanitized with sanitize_text_field above
120 + return $query->where('title', 'LIKE', '%' . $search . '%');
121 + })->take(20)->get();
122 +
123 + foreach ($tasks as $task) {
124 + $options[] = [
125 + 'id' => $task->id,
126 + 'title' => $task->title,
127 + 'board_id' => $task->board_id,
128 + 'subtask_groups' => $task->subtaskGroup
129 + ];
130 + }
131 +
74 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 +
75 137 $users = (new BoardService())->getAssigneesByBoard($boardId, $search);
76 - $options = $this->addUserDataAsSelectorOption($users);
138 + $options = $this->maskSelectorEmailsForViewer($this->addUserDataAsSelectorOption($users));
77 139 } else {
78 140 $options = apply_filters('fluent_boards/ajax_options_' . $optionKey, [], $search, $includedIds);
79 141 }
80 142
@@ -91,17 +153,51 @@
91 153 $options = [];
92 154 foreach ($users as $user) {
93 155 $options[] = [
94 156 'id' => $user->ID,
157 + 'email' => $user->user_email,
158 + 'name' => $user->display_name ?? $user->user_email,
95 159 'title' => $user->display_name . ' (' . $user->user_email . ')',
96 160 'photo' => get_avatar_url($user->user_email),
97 - 'left_side_value' => $user->display_name,
98 - 'right_side_value' => $user->user_email,
99 161 ];
100 162 }
101 163 return $options;
102 164 }
103 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 +
104 200 public function getCurrentUserPermissions()
105 201 {
106 202 try {
107 203 $currentUserBoards = Relation::query()
@@ -123,10 +219,10 @@
123 219
124 220 public function getUserPermission(Request $request)
125 221 {
126 222 try {
127 - $boardId = $request->getSafe('boardId');
128 - $userId = $request->getSafe('userId');
223 + $boardId = $request->getSafe('boardId', 'intval');
224 + $userId = $request->getSafe('userId', 'intval');
129 225
130 226 $boardUser = Relation::where('board_id', $boardId)
131 227 ->where('user_id', $userId)
132 228 ->where('status', 'ACTIVE')->first();
@@ -144,12 +240,12 @@
144 240
145 241 public function updatedUserPermission(Request $request)
146 242 {
147 243 try {
148 - $permission = $request->getSafe('userPermission');
149 - $updateType = $request->getSafe('updateType');
150 - $boardId = $request->getSafe('boardId');
151 - $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');
152 248
153 249 $boardUser = Relation::where('board_id', $boardId)->where('user_id', $userId)->status('ACTIVE')->first();
154 250
155 251 if ('Board Admin' == $permission) {
@@ -181,8 +277,9 @@
181 277 }
182 278
183 279 public function SetUserSuperAdmin($userId)
184 280 {
281 + $userId = absint($userId);
185 282 try {
186 283 $this->optionService->createSuperAdmin($userId);
187 284 return $this->sendSuccess([
188 285 'message' => __('Member has been set super admin successfully!', 'fluent-boards')
@@ -194,8 +291,9 @@
194 291 }
195 292
196 293 public function removeUserSuperAdmin($userId)
197 294 {
295 + $userId = absint($userId);
198 296 try {
199 297 $this->optionService->removeUserSuperAdmin($userId);
200 298
201 299 return $this->sendSuccess([
@@ -208,9 +306,9 @@
208 306
209 307 public function IsUserAllBoardAdmin(Request $request)
210 308 {
211 309 try {
212 - $userId = $request->getSafe('id');
310 + $userId = $request->getSafe('id', 'intval');
213 311 $isSuperAdmin = false;
214 312 $superAdmin = Relation::where('board_id', null)->where('user_id', $userId)->where('status', 'ACTIVE')->first();
215 313 $totalSuperAdmin = Relation::where('board_id', null)->where('status', 'ACTIVE')->count();
216 314 $permissions = [];
@@ -230,10 +328,11 @@
230 328 }
231 329
232 330 public function RemoveUserFromSuperAdmin(Request $request, $id)
233 331 {
332 + $id = absint($id);
234 333 try {
235 - $userId = $request->getSafe('id');
334 + $userId = $request->getSafe('id', 'intval');
236 335
237 336 $superAdmin = Relation::where('board_id', null)->where('user_id', $userId)->first();
238 337 $superAdmin->status = 'INACTIVE';
239 338 $superAdmin->save();
@@ -248,10 +347,10 @@
248 347
249 348 public function removeUserFromBoard(Request $request)
250 349 {
251 350 try {
252 - $boardId = $request->getSafe('boardId');
253 - $userId = $request->getSafe('userId');
351 + $boardId = $request->getSafe('boardId', 'intval');
352 + $userId = $request->getSafe('userId', 'intval');
254 353
255 354 $this->boardService->removeUserFromBoard($boardId, $userId);
256 355
257 356 if (!PermissionManager::isAdmin($userId)) {
@@ -268,9 +367,14 @@
268 367
269 368 public function addAsSuperAdmin(Request $request)
270 369 {
271 370 try {
272 - $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 + }
273 377 foreach ($userIds as $userId) {
274 378 $this->createSuperAdmin($userId);
275 379 }
276 380
@@ -299,10 +403,20 @@
299 403
300 404 public function addMembersInBoards(Request $request)
301 405 {
302 406 try {
303 - $userIds = $request->getSafe('memberIds');
304 - $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 + }
305 419
306 420 foreach ($userIds as $userId) {
307 421 foreach ($boardIds as $boardId) {
308 422 $this->boardService->addMembersInBoard($boardId, $userId);
@@ -320,9 +434,18 @@
320 434
321 435 public function updateGlobalNotificationSettings(Request $request)
322 436 {
323 437 try {
324 - $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 + }
325 448
326 449 $this->optionService->updateGlobalNotificationSettings($newSettings);
327 450
328 451 return $this->sendSuccess([
@@ -357,21 +480,29 @@
357 480 }
358 481
359 482 $boardId = $request->getSafe('boardId', 'intval');
360 483
361 - $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)
362 491 ->select(['foreign_id'])
363 492 ->groupBy('foreign_id');
364 493
365 494 if ($boardId) {
366 - $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);
367 499 }
368 500
369 - $members = [];
370 -
371 - $memberUserIds = $memberUserIds->get()
501 + $memberUserIds = $memberUserIdsQuery->get()
372 502 ->pluck('foreign_id')->toArray();
373 503
504 + $members = [];
374 505
375 506 if ($memberUserIds) {
376 507 $memberUsers = get_users([
377 508 'include' => $memberUserIds
@@ -421,70 +552,106 @@
421 552 'members' => $members
422 553 ];
423 554 }
424 555
425 - public function quickSearch()
556 + public function globalSearch()
426 557 {
427 558 $currentUserId = get_current_user_id();
428 559
429 - $query = sanitize_text_field($_REQUEST['query']);
430 - $query = strtolower($query);
431 - $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';
432 564
433 - $tasksQuery = Task::query()->where('parent_id', null)->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']);
434 - $isUserAdmin = PermissionManager::isAdmin($currentUserId);
565 + // Pagination parameters
566 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
567 + $taskPage = isset($_REQUEST['task_page']) ? max(1, (int)$_REQUEST['task_page']) : 0;
568 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
569 + $boardPage = isset($_REQUEST['board_page']) ? max(1, (int)$_REQUEST['board_page']) : 0;
570 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
571 + $perPage = isset($_REQUEST['per_page']) ? (int)$_REQUEST['per_page'] : 20;
572 + $perPage = max(1, min(100, $perPage));
435 573
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();
574 + // Build base queries
575 + $firstThreeChars = substr($query, 0, 3);
576 + $firstNineChars = substr($query, 0, 9);
438 577
578 + if($firstThreeChars == 'id:') {
579 + $idPart = preg_replace('/[^a-zA-Z0-9]/', '', substr($query, 3));
580 + $tasksQuery = Task::query()->where('parent_id', null)->whereRaw('id LIKE ?', ['%' . $idPart . '%']);
581 + $boardQuery = Board::query()->whereRaw('id LIKE ?', ['%' . $idPart . '%']);
582 + }elseif($firstNineChars == 'archived:') {
583 + $archivedPart = trim(substr($query, 9));
584 + $tasksQuery = Task::query()->where('parent_id', null)->whereNotNull('archived_at')->whereRaw('LOWER(title) LIKE ?', ['%' . $archivedPart . '%']);
585 + $boardQuery = Board::query()->whereNotNull('archived_at')->whereRaw('LOWER(title) LIKE ?', ['%' . $archivedPart . '%']);
586 + } else {
587 + $tasksQuery = Task::query()->where('parent_id', null)->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']);
588 + $boardQuery = Board::query()->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']);
589 + }
590 +
591 + // Apply scope and permissions
592 + $isUserAdmin = PermissionManager::isAdmin($currentUserId);
439 593 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 {
594 + if (!$isUserAdmin) {
445 595 $boardIds = PermissionManager::getBoardIdsForUser($currentUserId);
446 - $boards = $boardQuery->whereIn('id', $boardIds)->get();
447 - $tasks = $tasksQuery->whereIn('board_id', $boardIds)->get();
596 + $boardQuery->whereIn('id', $boardIds);
597 + $tasksQuery->whereIn('board_id', $boardIds);
448 598 }
449 599 } 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();
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);
454 606 } else {
455 - // Out of permission scope search
456 - $tasks = [];
607 + $tasksQuery->where('id', -1); // Force no results
457 608 }
458 609 }
459 610
611 + $allActiveBoardsIds = Board::query()->where('archived_at', null)->pluck('id')->toArray();
460 612
613 + $boards = [];
614 + $tasks = [];
615 + $totalBoards = 0;
616 + $totalTasks = 0;
617 + $formattedBoards = [];
461 618 $formattedTasks = [];
462 - $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 +
463 634 foreach ($boards as $board) {
464 635 $formattedBoards[] = [
465 636 'type' => 'board',
466 637 'id' => $board->id,
467 638 'title' => $board->title,
468 - 'description' => $board->description,
469 - 'url' => Helper::getBoardUrl($board->id)
639 + 'description' => DescriptionMarkdownConverter::normalize($board->description),
470 640 ];
471 641 }
472 642 foreach ($tasks as $task) {
473 -
474 643 if (!in_array($task->board_id, $allActiveBoardsIds)) {
475 - // if the task is not in an active board, skip it
476 644 continue;
477 645 }
478 646
479 647 $board = $task->board;
480 -
481 648 $formattedTasks[] = [
482 649 'type' => 'task',
483 650 'id' => $task->id,
484 651 'title' => $task->title,
485 - 'description' => $task->description,
486 - 'url' => Helper::getTaskUrl($task->id, $board->id),
652 + 'description' => DescriptionMarkdownConverter::normalize($task->description),
653 + 'board_id' => $task->board_id,
487 654 'board' => [
488 655 'id' => $board->id,
489 656 'title' => $board->title,
490 657 'url' => Helper::getBoardUrl($board->id)
@@ -492,51 +659,101 @@
492 659 'stage' => [
493 660 'id' => $task->stage_id,
494 661 'title' => $task->stage->title ?? '',
495 662 ],
496 -
497 663 ];
498 664 }
665 +
499 666 return $this->sendSuccess([
500 - 'tasks' => $formattedTasks,
501 - '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 + ]
502 681 ], 200);
503 -
504 682 }
505 683
506 - public function getDashboardViewSettings()
684 + public function getDashboardViewSettings(Request $request)
507 685 {
508 - try {
686 + $view = $request->getSafe('view', 'sanitize_text_field');
687 +
688 + if ($view == 'kanbanview') {
509 689 $globalSettings = $this->optionService->getDashboardViewSettings();
510 - if ($globalSettings->value)
511 - $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 + }
512 698
513 - return $this->sendSuccess([
514 - 'currentSettings' => $currentSettings,
515 - ], 200);
516 - } catch (\Exception $e) {
517 - return $this->sendError($e->getMessage(), 404);
518 - }
699 + if ($globalSettings->value)
700 + $currentSettings = maybe_unserialize($globalSettings->value);
701 +
702 + return $this->sendSuccess([
703 + 'currentSettings' => $currentSettings,
704 + ], 200);
519 705 }
520 706
521 707 public function updateDashboardViewSettings(Request $request)
522 708 {
523 - try {
524 - $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');
525 727
526 - $this->optionService->updateDashboardViewSettings($newSettings);
728 + $this->optionService->updateDashboardViewSettings($newSettings, $view);
527 729
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);
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');
533 736 }
737 +
738 + return $this->sendSuccess([
739 + 'message' => $message,
740 + ], 201);
534 741 }
535 742
536 743
537 744 public function getAddonsSettings()
538 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 +
539 756 $addOns = [
540 757 'fluent-crm' => [
541 758 'title' => __('FluentCRM', 'fluent-boards'),
542 759 'logo' => fluent_boards_mix('images/addons/fluent-crm.svg'),
@@ -543,9 +760,10 @@
543 760 'is_installed' => defined('FLUENTCRM'),
544 761 'learn_more_url' => 'https://fluentcrm.com/',
545 762 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluentcrm/',
546 763 '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')
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')
548 766 ],
549 767 'fluentform' => [
550 768 'title' => __('Fluent Forms', 'fluent-boards'),
551 769 'logo' => fluent_boards_mix('images/addons/fluentform.png'),
@@ -552,9 +770,10 @@
552 770 'is_installed' => defined('FLUENTFORM'),
553 771 'learn_more_url' => 'https://wordpress.org/plugins/fluentform/',
554 772 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluent-forms/',
555 773 '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')
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')
557 776 ],
558 777 'fluent-support' => [
559 778 'title' => __('Fluent Support', 'fluent-boards'),
560 779 'logo' => fluent_boards_mix('images/addons/fluent-support.svg'),
@@ -562,9 +781,10 @@
562 781 'learn_more_url' => 'https://wordpress.org/plugins/fluent-connect/',
563 782 'settings_url' => admin_url('admin.php?page=fluent-support#/'),
564 783 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluentsupport/',
565 784 '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')
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')
567 787 ],
568 788 'fluent-smtp' => [
569 789 'title' => __('Fluent SMTP', 'fluent-boards'),
570 790 'logo' => fluent_boards_mix('images/addons/fluent-smtp.svg'),
@@ -571,12 +791,28 @@
571 791 'is_installed' => defined('FLUENTMAIL'),
572 792 'learn_more_url' => 'https://wordpress.org/plugins/fluent-smtp/',
573 793 'associate_doc' => admin_url('options-general.php?page=fluent-mail#/'),
574 794 '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')
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')
576 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 + ],
577 811 ];
578 812
813 + $addOns = apply_filters('fluent_boards/addons_settings', $addOns);
814 +
579 815 $modules = fluent_boards_get_pref_settings(false);
580 816
581 817 if (empty($modules['frontend']['render_type'])) {
582 818 $modules['frontend']['render_type'] = 'standalone';
@@ -597,16 +833,27 @@
597 833 'message' => __('This feature is only available in Fluent Boards Pro', 'fluent-boards')
598 834 ]);
599 835 }
600 836
601 - $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 + }
602 845
603 846 $prefSettings = fluent_boards_get_pref_settings(false);
604 847
605 - $settings = wp_parse_args($settings, $prefSettings);
848 + $settings = wp_parse_args($rawSettings, $prefSettings);
606 849
607 850 $settings = Arr::only($settings, array_keys($prefSettings));
608 - $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 + }
609 856
610 857 if (empty($settings['frontend']['slug'])) {
611 858 $settings['frontend']['slug'] = 'projects';
612 859 }
@@ -618,8 +865,12 @@
618 865 do_action('fluent_boards/saving_addons', $settings, $prefSettings);
619 866
620 867 update_option('fluent_boards_modules', $settings, 'yes');
621 868
869 + if (isset($settings['recurring_task']['enabled']) && $settings['recurring_task']['enabled'] == 'no') {
870 + do_action('fluent_boards/recurring_task_disabled');
871 + }
872 +
622 873 return $this->sendSuccess([
623 874 'message' => __('Settings are saved', 'fluent-boards'),
624 875 'featureModules' => $settings
625 876 ]);
@@ -628,15 +879,15 @@
628 879 public function installPlugin(Request $request)
629 880 {
630 881 if (!current_user_can('install_plugins')) {
631 882 return $this->sendError([
632 - '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')
633 884 ]);
634 885 }
635 886
636 887 $plugin = $request->getSafe('plugin', 'sanitize_text_field');
637 888
638 - $acceptedPlugins = [
889 + $acceptedFreePlugins = [
639 890 'fluent-crm' => 'fluent-crm.php',
640 891 'fluentform' => 'fluentform.php',
641 892 'fluent-support' => 'fluent-support.php',
642 893 'fluent-smtp' => 'fluent-smtp.php'
@@ -641,8 +892,10 @@
641 892 'fluent-support' => 'fluent-support.php',
642 893 'fluent-smtp' => 'fluent-smtp.php'
643 894 ];
644 895
896 + $acceptedPlugins = apply_filters('fluent_boards/accepted_plugins', $acceptedFreePlugins);
897 +
645 898 if (!isset($acceptedPlugins[$plugin])) {
646 899 return $this->sendError([
647 900 'message' => __('Invalid plugin', 'fluent-boards')
648 901 ]);
@@ -647,16 +900,20 @@
647 900 'message' => __('Invalid plugin', 'fluent-boards')
648 901 ]);
649 902 }
650 903
651 -
652 904 $pluginToInstall = [
653 - 'name' => __('Fluent Plugin', 'fluent-crm'),
905 + 'name' => __('Fluent Plugin', 'fluent-boards'),
654 906 'repo-slug' => $plugin,
655 907 'file' => $acceptedPlugins[$plugin],
656 908 ];
657 909
658 - $this->backgroundInstaller($pluginToInstall, $plugin);
910 + // if plugin in free list then run background intaller otherwise call an action to install
911 + if (isset($acceptedFreePlugins[$plugin])) {
912 + $this->backgroundInstaller($pluginToInstall, $plugin);
913 + } else {
914 + do_action('fluent_boards/install_plugin', $pluginToInstall, $plugin);
915 + }
659 916
660 917 return $this->sendSuccess([
661 918 'message' => __('Plugin is being installed', 'fluent-boards')
662 919 ]);
@@ -666,8 +923,19 @@
666 923 {
667 924 return file_exists(WP_PLUGIN_DIR . '/' . $plugin);
668 925 }
669 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 +
670 938 private function backgroundInstaller($plugin_to_install, $plugin_id)
671 939 {
672 940 if (!empty($plugin_to_install['repo-slug'])) {
673 941 require_once ABSPATH . 'wp-admin/includes/file.php';
@@ -719,9 +987,9 @@
719 987 )
720 988 );
721 989
722 990 if (is_wp_error($plugin_information)) {
723 - throw new \Exception($plugin_information->get_error_message());
991 + throw new \Exception(esc_html($plugin_information->get_error_message()));
724 992 }
725 993
726 994 $package = $plugin_information->download_link;
727 995 $download = $upgrader->download_package($package);
@@ -726,15 +994,15 @@
726 994 $package = $plugin_information->download_link;
727 995 $download = $upgrader->download_package($package);
728 996
729 997 if (is_wp_error($download)) {
730 - throw new \Exception($download->get_error_message());
998 + throw new \Exception(esc_html($download->get_error_message()));
731 999 }
732 1000
733 1001 $working_dir = $upgrader->unpack_package($download, true);
734 1002
735 1003 if (is_wp_error($working_dir)) {
736 - throw new \Exception($working_dir->get_error_message());
1004 + throw new \Exception(esc_html($working_dir->get_error_message()));
737 1005 }
738 1006
739 1007 $result = $upgrader->install_package(
740 1008 array(
@@ -750,9 +1018,9 @@
750 1018 )
751 1019 );
752 1020
753 1021 if (is_wp_error($result)) {
754 - throw new \Exception($result->get_error_message());
1022 + throw new \Exception(esc_html($result->get_error_message()));
755 1023 }
756 1024
757 1025 $activate = true;
758 1026
@@ -770,9 +1038,9 @@
770 1038 try {
771 1039 $result = activate_plugin($installed ? $installed_plugins[$plugin_file] : $plugin_slug . '/' . $plugin_file);
772 1040
773 1041 if (is_wp_error($result)) {
774 - throw new \Exception($result->get_error_message());
1042 + throw new \Exception(esc_html($result->get_error_message()));
775 1043 }
776 1044 } catch (\Exception $e) {
777 1045 }
778 1046 }
@@ -813,9 +1081,10 @@
813 1081 $pages = [];
814 1082 foreach ($allPages as $page) {
815 1083 $pages[] = [
816 1084 'id' => $page->ID,
817 - '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))
818 1087 ];
819 1088 }
820 1089
821 1090 return $this->sendSuccess([
@@ -820,7 +1089,66 @@
820 1089
821 1090 return $this->sendSuccess([
822 1091 'pages' => $pages
823 1092 ]);
1093 + }
1094 +
1095 + public function getGeneralSettings()
1096 + {
1097 + $settings = fluent_boards_get_option('general_settings', []);
1098 +
1099 + return $this->sendSuccess([
1100 + 'settings' => $settings,
1101 + 'server_timezone' => \wp_timezone_string()
1102 + ]);
1103 +
1104 + }
1105 +
1106 + public function saveGeneralSettings(Request $request)
1107 + {
1108 + // check for pro version
1109 + if (!defined('FLUENT_BOARDS_PRO')) {
1110 + return $this->sendError([
1111 + 'message' => __('This feature is only available in Fluent Boards Pro. Please upgrade.', 'fluent-boards')
1112 + ]);
1113 + }
1114 + // updatedSettings is an array, sanitize each element
1115 + $rawSettings = $request->get('updatedSettings', []);
1116 + $settings = [];
1117 + if (is_array($rawSettings)) {
1118 + foreach ($rawSettings as $key => $value) {
1119 + $sanitizedKey = sanitize_text_field($key);
1120 + // Value could be string, boolean, or number - sanitize appropriately
1121 + if (is_string($value)) {
1122 + $sanitizedValue = sanitize_text_field($value);
1123 + } elseif (is_bool($value) || is_numeric($value)) {
1124 + $sanitizedValue = $value;
1125 + } else {
1126 + $sanitizedValue = sanitize_text_field((string)$value);
1127 + }
1128 + $settings[$sanitizedKey] = $sanitizedValue;
1129 + }
1130 + }
1131 +
1132 + $settings = apply_filters('fluent_boards/save_general_settings', $settings);
1133 +
1134 + $savedSettings = fluent_boards_update_option('general_settings', $settings);
1135 + $savedGeneralSettings = \maybe_unserialize($savedSettings->value);
1136 +
1137 + $scheduleHandler = new ProScheduleHandler();
1138 +
1139 + $dailyReminderEnabled = $savedGeneralSettings['daily_reminder_enabled'] ?? false;
1140 +
1141 + if (filter_var($dailyReminderEnabled, FILTER_VALIDATE_BOOLEAN)) {
1142 + // force schedule from this settings update
1143 + $scheduleHandler->clearDailyTaskReminderScheduler();
1144 + $scheduleHandler->scheduleDailyTaskReminder();
1145 + }
1146 +
1147 + return $this->sendSuccess([
1148 + 'settings' => $savedGeneralSettings,
1149 + 'message' => __('Settings are saved', 'fluent-boards')
1150 + ]);
1151 +
824 1152 }
825 1153
826 1154 }