PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.0.4
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.0.4
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
fluent-boards / app / Http / Controllers / OptionsController.php

OptionsController.php in FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration 2.0.4, at app/Http/Controllers/OptionsController.php

1,131 lines 44.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBoards\App\Http\Controllers;
4
5 use FluentBoards\App\App;
6 use FluentBoards\App\Models\Meta;
7 use FluentBoards\App\Models\Task;
8 use FluentBoards\App\Models\User;
9 use FluentBoards\App\Models\Board;
10 use FluentBoards\App\Models\Relation;
11 use FluentBoards\App\Services\BoardService;
12 use FluentBoards\App\Services\DescriptionMarkdownConverter;
13 use FluentBoards\App\Services\Helper;
14 use FluentBoards\App\Services\OptionService;
15 use FluentBoards\App\Services\Constant;
16 use FluentBoards\App\Services\PermissionManager;
17 use FluentBoards\Framework\Http\Request\Request;
18 use FluentBoards\Framework\Support\Arr;
19 use FluentBoardsPro\App\Hooks\Handlers\ProScheduleHandler;
20
21 class OptionsController extends Controller
22 {
23 private $boardService;
24 private $optionService;
25
26 public function __construct(BoardService $boardService, OptionService $optionService)
27 {
28 parent::__construct();
29 $this->boardService = $boardService;
30 $this->optionService = $optionService;
31 }
32
33 public function selectorOptions(Request $request)
34 {
35 try {
36 $optionKey = $request->getSafe('option_key', 'sanitize_text_field');
37 $search = $request->getSafe('search', 'sanitize_text_field');
38 $includedIds = $request->getSafe('values');
39 $boardId = $request->getSafe('board_id', 'intval');
40
41 $options = [];
42 if ('users' === $optionKey || 'task_assignees' === $optionKey) { // no ajax/code is designed to handle this eventually will goto else
43
44 if (!PermissionManager::isBoardManager($boardId)) {
45 throw new \Exception(esc_html__('You do not have permission to access this route', 'fluent-boards'));
46 }
47
48 if (!defined('FLUENT_BOARDS_PRO')) {
49 // get who has 'manage_options' capability
50 $users = PermissionManager::getAll_WP_Admins($search);
51
52 } else {
53 // Search by user login, email, and nicename, first_name , last_name
54 $users = Helper::searchWordPressUsers($search);
55 $users = Helper::sanitizeUsersArray($users, $boardId);
56
57 }
58
59 $options = $this->addUserDataAsSelectorOption($users);
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
78 } elseif ('boards' === $optionKey) {
79 $boards = Board::query()
80 ->when($search, function ($query) use ($search) {
81 // $search is already sanitized with sanitize_text_field above
82 return $query->where('title', 'LIKE', '%' . $search . '%');
83 })->take(20)->get();
84
85 foreach ($boards as $board) {
86 $options[] = [
87 'id' => $board->id,
88 'title' => $board->title,
89 'left_side_value' => $board->title,
90 'right_side_value' => $board->slug,
91 ];
92 }
93 } elseif ('tasks' === $optionKey) {
94 if (!PermissionManager::userHasPermission($boardId)) {
95 throw new \Exception(esc_html__('You do not have permission to access this route', 'fluent-boards'));
96 }
97
98 // $boardId is already sanitized with intval above
99 $tasks = Task::where('board_id', $boardId)
100 ->whereNull('archived_at')
101 ->whereNull('parent_id')
102 ->when($search, function ($query) use ($search) {
103 // $search is already sanitized with sanitize_text_field above
104 return $query->where('title', 'LIKE', '%' . $search . '%');
105 })->take(20)->get();
106
107 foreach ($tasks as $task) {
108 $options[] = [
109 'id' => $task->id,
110 'title' => $task->title,
111 'board_id' => $task->board_id,
112 'subtask_groups' => $task->subtaskGroup
113 ];
114 }
115
116 } elseif ('assigned_in_task' == $optionKey) {
117 if (!PermissionManager::userHasPermission($boardId)) {
118 throw new \Exception(esc_html__('You do not have permission to access this route', 'fluent-boards'));
119 }
120
121 $users = (new BoardService())->getAssigneesByBoard($boardId, $search);
122 $options = $this->addUserDataAsSelectorOption($users);
123 } else {
124 $options = apply_filters('fluent_boards/ajax_options_' . $optionKey, [], $search, $includedIds);
125 }
126
127 return $this->sendSuccess([
128 'options' => $options,
129 ], 200);
130 } catch (\Exception $e) {
131 return $this->sendError($e->getMessage(), 404);
132 }
133 }
134
135 private function addUserDataAsSelectorOption($users)
136 {
137 $options = [];
138 foreach ($users as $user) {
139 $options[] = [
140 'id' => $user->ID,
141 'email' => $user->user_email,
142 'name' => $user->display_name ?? $user->user_email,
143 'title' => $user->display_name . ' (' . $user->user_email . ')',
144 'photo' => get_avatar_url($user->user_email),
145 ];
146 }
147 return $options;
148 }
149
150 /**
151 * Obfuscate emails in selector options for viewers who lack the `list_users`
152 * capability, keeping the current user's own email visible.
153 */
154 private function maskSelectorEmailsForViewer($options)
155 {
156 if (current_user_can('list_users')) {
157 return $options;
158 }
159
160 $currentUser = wp_get_current_user();
161 $currentUserEmail = ($currentUser && isset($currentUser->user_email)) ? $currentUser->user_email : '';
162
163 foreach ($options as $index => $option) {
164 $email = $option['email'] ?? '';
165
166 if ($email === '' || $email === $currentUserEmail) {
167 continue;
168 }
169
170 $maskedEmail = Helper::obfuscateEmail($email);
171
172 // When there's no display name the raw email doubles as the name.
173 if (($option['name'] ?? '') === $email) {
174 $options[$index]['name'] = $maskedEmail;
175 }
176
177 $options[$index]['email'] = $maskedEmail;
178 $options[$index]['title'] = ($options[$index]['name'] ?? $maskedEmail) . ' (' . $maskedEmail . ')';
179 }
180
181 return $options;
182 }
183
184 public function getCurrentUserPermissions()
185 {
186 try {
187 $currentUserBoards = Relation::query()
188 ->where('user_id', get_current_user_id())
189 ->whereNotNull('board_id')
190 ->where('status', 'ACTIVE')
191 ->get();
192 foreach ($currentUserBoards as &$currentUserBoardPermission) {
193 $currentUserBoardPermission->permissions = \maybe_unserialize($currentUserBoardPermission->permissions);
194 }
195
196 return $this->sendSuccess(
197 $currentUserBoards, 200
198 );
199 } catch (\Exception $e) {
200 return $this->sendError($e->getMessage(), 404);
201 }
202 }
203
204 public function getUserPermission(Request $request)
205 {
206 try {
207 $boardId = $request->getSafe('boardId', 'intval');
208 $userId = $request->getSafe('userId', 'intval');
209
210 $boardUser = Relation::where('board_id', $boardId)
211 ->where('user_id', $userId)
212 ->where('status', 'ACTIVE')->first();
213 if (!$boardUser->is_admin) {
214 $boardUser->permissions = \maybe_unserialize($boardUser->permissions);
215 }
216
217 return $this->sendSuccess(
218 $boardUser, 200
219 );
220 } catch (\Exception $e) {
221 return $this->sendError($e->getMessage(), 404);
222 }
223 }
224
225 public function updatedUserPermission(Request $request)
226 {
227 try {
228 $permission = $request->getSafe('userPermission', 'sanitize_text_field');
229 $updateType = $request->getSafe('updateType', 'sanitize_text_field');
230 $boardId = $request->getSafe('boardId', 'intval');
231 $userId = $request->getSafe('userId', 'intval');
232
233 $boardUser = Relation::where('board_id', $boardId)->where('user_id', $userId)->status('ACTIVE')->first();
234
235 if ('Board Admin' == $permission) {
236 $boardUser->is_admin = 'add' == $updateType ? 1 : 0;
237 $boardUser->save();
238 } else {
239 if (0 == $boardUser->is_admin) {
240 $permissionsAlreadyHave = maybe_unserialize($boardUser->permissions);
241 if (!in_array($permission, $permissionsAlreadyHave) && 'add' == $updateType) {
242 array_push($permissionsAlreadyHave, $permission);
243 } elseif (in_array($permission, $permissionsAlreadyHave) && 'remove' == $updateType) {
244 if (($key = array_search($permission, $permissionsAlreadyHave)) !== false) {
245 unset($permissionsAlreadyHave[$key]);
246 }
247 }
248 $boardUser->permissions = serialize($permissionsAlreadyHave);
249 $boardUser->save();
250 }
251 }
252
253 $boardUser->permissions = maybe_unserialize($boardUser->permissions);
254
255 return $this->sendSuccess(
256 $boardUser, 200
257 );
258 } catch (\Exception $e) {
259 return $this->sendError($e->getMessage(), 404);
260 }
261 }
262
263 public function SetUserSuperAdmin($userId)
264 {
265 $userId = absint($userId);
266 try {
267 $this->optionService->createSuperAdmin($userId);
268 return $this->sendSuccess([
269 'message' => __('Member has been set super admin successfully!', 'fluent-boards')
270 ], 200);
271
272 } catch (\Exception $e) {
273 return $this->sendError($e->getMessage(), 404);
274 }
275 }
276
277 public function removeUserSuperAdmin($userId)
278 {
279 $userId = absint($userId);
280 try {
281 $this->optionService->removeUserSuperAdmin($userId);
282
283 return $this->sendSuccess([
284 'message' => __('User has been removed as super admin successfully!', 'fluent-boards'),
285 ], 200);
286 } catch (\Exception $e) {
287 return $this->sendError($e->getMessage(), 404);
288 }
289 }
290
291 public function IsUserAllBoardAdmin(Request $request)
292 {
293 try {
294 $userId = $request->getSafe('id', 'intval');
295 $isSuperAdmin = false;
296 $superAdmin = Relation::where('board_id', null)->where('user_id', $userId)->where('status', 'ACTIVE')->first();
297 $totalSuperAdmin = Relation::where('board_id', null)->where('status', 'ACTIVE')->count();
298 $permissions = [];
299 if ($superAdmin) {
300 $isSuperAdmin = true;
301 $permissions = \maybe_unserialize($superAdmin->permissions);
302 }
303
304 return $this->sendSuccess([
305 'allBoardAdmin' => $isSuperAdmin,
306 'permissions' => $permissions,
307 'numberOfSuperAdmin' => $totalSuperAdmin,
308 ], 200);
309 } catch (\Exception $e) {
310 return $this->sendError($e->getMessage(), 404);
311 }
312 }
313
314 public function RemoveUserFromSuperAdmin(Request $request, $id)
315 {
316 $id = absint($id);
317 try {
318 $userId = $request->getSafe('id', 'intval');
319
320 $superAdmin = Relation::where('board_id', null)->where('user_id', $userId)->first();
321 $superAdmin->status = 'INACTIVE';
322 $superAdmin->save();
323
324 return $this->sendSuccess([
325 'message' => __('User removed as super admin', 'fluent-boards'),
326 ], 201);
327 } catch (\Exception $e) {
328 return $this->sendError($e->getMessage(), 404);
329 }
330 }
331
332 public function removeUserFromBoard(Request $request)
333 {
334 try {
335 $boardId = $request->getSafe('boardId', 'intval');
336 $userId = $request->getSafe('userId', 'intval');
337
338 $this->boardService->removeUserFromBoard($boardId, $userId);
339
340 if (!PermissionManager::isAdmin($userId)) {
341 $this->boardService->removeFromRecentlyOpened($boardId, $userId);
342 }
343
344 return $this->sendSuccess([
345 'message' => __('User Removed from Board successfully!', 'fluent-boards'),
346 ], 201);
347 } catch (\Exception $e) {
348 return $this->sendError($e->getMessage(), 404);
349 }
350 }
351
352 public function addAsSuperAdmin(Request $request)
353 {
354 try {
355 $rawUserIds = $request->getSafe('memberIds');
356 // Sanitize array of user IDs
357 $userIds = [];
358 if (is_array($rawUserIds)) {
359 $userIds = array_filter(array_map('intval', $rawUserIds));
360 }
361 foreach ($userIds as $userId) {
362 $this->createSuperAdmin($userId);
363 }
364
365 return $this->sendSuccess([
366 'message' => __('Fluent boards admin added', 'fluent-boards'),
367 ], 201);
368 } catch (\Exception $e) {
369 return $this->sendError($e->getMessage(), 404);
370 }
371 }
372
373 private function createSuperAdmin($userId)
374 {
375 try {
376 $existUser = Meta::where('object_id', $userId)->first();
377 if (!$existUser) {
378 $meta = new Meta();
379 $meta->object_id = $userId;
380 $meta->object_type = Constant::FLUENT_BOARD_ADMIN;
381 $meta->save();
382 }
383 } catch (\Exception $e) {
384 return $this->sendError($e->getMessage(), 404);
385 }
386 }
387
388 public function addMembersInBoards(Request $request)
389 {
390 try {
391 $rawUserIds = $request->getSafe('memberIds');
392 $rawBoardIds = $request->getSafe('boardIds');
393
394 // Sanitize arrays of IDs
395 $userIds = [];
396 if (is_array($rawUserIds)) {
397 $userIds = array_filter(array_map('intval', $rawUserIds));
398 }
399 $boardIds = [];
400 if (is_array($rawBoardIds)) {
401 $boardIds = array_filter(array_map('intval', $rawBoardIds));
402 }
403
404 foreach ($userIds as $userId) {
405 foreach ($boardIds as $boardId) {
406 $this->boardService->addMembersInBoard($boardId, $userId);
407 }
408 }
409
410 return $this->sendSuccess([
411 'message' => __('Members added to boards', 'fluent-boards'),
412 ], 201);
413 } catch (\Exception $e) {
414 return $this->sendError($e->getMessage(), 404);
415 }
416 }
417
418
419 public function updateGlobalNotificationSettings(Request $request)
420 {
421 try {
422 // updatedSettings is an array, sanitize each element
423 $rawSettings = $request->get('updatedSettings');
424 $newSettings = [];
425 if (is_array($rawSettings)) {
426 foreach ($rawSettings as $key => $value) {
427 $sanitizedKey = sanitize_text_field($key);
428 $sanitizedValue = sanitize_text_field($value);
429 $newSettings[$sanitizedKey] = $sanitizedValue;
430 }
431 }
432
433 $this->optionService->updateGlobalNotificationSettings($newSettings);
434
435 return $this->sendSuccess([
436 'message' => __("Notification settings are updated", 'fluent-boards'),
437 ], 201);
438 } catch (\Exception $e) {
439 return $this->sendError($e->getMessage(), 404);
440 }
441 }
442
443 public function getGlobalNotificationSettings()
444 {
445 try {
446 $globalSettings = $this->optionService->getGlobalNotificationSettings();
447 if ($globalSettings->value)
448 $currentSettings = maybe_unserialize($globalSettings->value);
449
450 return $this->sendSuccess([
451 'currentSettings' => $currentSettings,
452 ], 200);
453 } catch (\Exception $e) {
454 return $this->sendError($e->getMessage(), 404);
455 }
456 }
457
458 public function getBoardMembers(Request $request)
459 {
460 if (!PermissionManager::userHasAnyBoardAccess()) {
461 return $this->sendError([
462 'message' => __('You do not have permission to access this route', 'fluent-boards')
463 ]);
464 }
465
466 $boardId = $request->getSafe('boardId', 'intval');
467
468 $memberUserIds = Relation::where('object_type', 'board_user')
469 ->select(['foreign_id'])
470 ->groupBy('foreign_id');
471
472 if ($boardId) {
473 $memberUserIds = $memberUserIds->where('object_id', $boardId);
474 }
475
476 $members = [];
477
478 $memberUserIds = $memberUserIds->get()
479 ->pluck('foreign_id')->toArray();
480
481
482 if ($memberUserIds) {
483 $memberUsers = get_users([
484 'include' => $memberUserIds
485 ]);
486
487
488 foreach ($memberUsers as $memberUser) {
489 $name = trim($memberUser->first_name . ' ' . $memberUser->last_name);
490 if (!$name) {
491 $name = $memberUser->display_name;
492 }
493
494 $members[$memberUser->ID] = [
495 'ID' => $memberUser->ID,
496 'display_name' => $name,
497 'photo' => get_avatar_url($memberUser->user_email)
498 ];
499 }
500
501 }
502
503 $adminUsers = get_users([
504 'role' => 'administrator',
505 'exclude' => $memberUserIds
506 ]);
507
508 foreach ($adminUsers as $user) {
509 $name = trim($user->first_name . ' ' . $user->last_name);
510 if (!$name) {
511 $name = $user->display_name;
512 }
513 $members[$user->ID] = [
514 'ID' => $user->ID,
515 'display_name' => $name,
516 'photo' => get_avatar_url($user->user_email)
517 ];
518 }
519
520 $members = array_values($members);
521
522 // sort members by name
523 usort($members, function ($a, $b) {
524 return strcmp($a['display_name'], $b['display_name']);
525 });
526
527 return [
528 'members' => $members
529 ];
530 }
531
532 public function globalSearch()
533 {
534 $currentUserId = get_current_user_id();
535
536 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
537 $query = isset($_REQUEST['query']) ? strtolower(sanitize_text_field(wp_unslash($_REQUEST['query']))) : '';
538 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
539 $scope = isset($_REQUEST['scope']) ? sanitize_text_field(wp_unslash($_REQUEST['scope'])) : 'all';
540
541 // Pagination parameters
542 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
543 $taskPage = isset($_REQUEST['task_page']) ? max(1, (int)$_REQUEST['task_page']) : 0;
544 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
545 $boardPage = isset($_REQUEST['board_page']) ? max(1, (int)$_REQUEST['board_page']) : 0;
546 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
547 $perPage = isset($_REQUEST['per_page']) ? (int)$_REQUEST['per_page'] : 20;
548 $perPage = max(1, min(100, $perPage));
549
550 // Build base queries
551 $firstThreeChars = substr($query, 0, 3);
552 $firstNineChars = substr($query, 0, 9);
553
554 if($firstThreeChars == 'id:') {
555 $idPart = preg_replace('/[^a-zA-Z0-9]/', '', substr($query, 3));
556 $tasksQuery = Task::query()->where('parent_id', null)->whereRaw('id LIKE ?', ['%' . $idPart . '%']);
557 $boardQuery = Board::query()->whereRaw('id LIKE ?', ['%' . $idPart . '%']);
558 }elseif($firstNineChars == 'archived:') {
559 $archivedPart = trim(substr($query, 9));
560 $tasksQuery = Task::query()->where('parent_id', null)->whereNotNull('archived_at')->whereRaw('LOWER(title) LIKE ?', ['%' . $archivedPart . '%']);
561 $boardQuery = Board::query()->whereNotNull('archived_at')->whereRaw('LOWER(title) LIKE ?', ['%' . $archivedPart . '%']);
562 } else {
563 $tasksQuery = Task::query()->where('parent_id', null)->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']);
564 $boardQuery = Board::query()->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']);
565 }
566
567 // Apply scope and permissions
568 $isUserAdmin = PermissionManager::isAdmin($currentUserId);
569 if ($scope == 'all') {
570 if (!$isUserAdmin) {
571 $boardIds = PermissionManager::getBoardIdsForUser($currentUserId);
572 $boardQuery->whereIn('id', $boardIds);
573 $tasksQuery->whereIn('board_id', $boardIds);
574 }
575 } else {
576 // For 'current_board' scope, we don't search boards
577 $boardQuery->where('id', -1);
578
579 $inBoard = absint($scope);
580 if ($isUserAdmin || in_array($inBoard, PermissionManager::getBoardIdsForUser($currentUserId))) {
581 $tasksQuery->where('board_id', $inBoard);
582 } else {
583 $tasksQuery->where('id', -1); // Force no results
584 }
585 }
586
587 $allActiveBoardsIds = Board::query()->where('archived_at', null)->pluck('id')->toArray();
588
589 $boards = [];
590 $tasks = [];
591 $totalBoards = 0;
592 $totalTasks = 0;
593 $formattedBoards = [];
594 $formattedTasks = [];
595
596 // Fetch Boards if requested
597 if ($boardPage > 0) {
598 $totalBoards = $boardQuery->count();
599 $boardOffset = ($boardPage - 1) * $perPage;
600 $boards = $boardQuery->skip($boardOffset)->take($perPage)->get();
601 }
602
603 // Fetch Tasks if requested
604 if ($taskPage > 0) {
605 $totalTasks = $tasksQuery->count();
606 $taskOffset = ($taskPage - 1) * $perPage;
607 $tasks = $tasksQuery->skip($taskOffset)->take($perPage)->get();
608 }
609
610 foreach ($boards as $board) {
611 $formattedBoards[] = [
612 'type' => 'board',
613 'id' => $board->id,
614 'title' => $board->title,
615 'description' => DescriptionMarkdownConverter::normalize($board->description),
616 ];
617 }
618 foreach ($tasks as $task) {
619 if (!in_array($task->board_id, $allActiveBoardsIds)) {
620 continue;
621 }
622
623 $board = $task->board;
624 $formattedTasks[] = [
625 'type' => 'task',
626 'id' => $task->id,
627 'title' => $task->title,
628 'description' => DescriptionMarkdownConverter::normalize($task->description),
629 'board_id' => $task->board_id,
630 'board' => [
631 'id' => $board->id,
632 'title' => $board->title,
633 'url' => Helper::getBoardUrl($board->id)
634 ],
635 'stage' => [
636 'id' => $task->stage_id,
637 'title' => $task->stage->title ?? '',
638 ],
639 ];
640 }
641
642 return $this->sendSuccess([
643 'tasks' => [
644 'data' => $formattedTasks,
645 'current_page' => $taskPage,
646 'per_page' => $perPage,
647 'total' => $totalTasks,
648 'last_page' => (int) ceil($totalTasks / $perPage)
649 ],
650 'boards' => [
651 'data' => $formattedBoards,
652 'current_page' => $boardPage,
653 'per_page' => $perPage,
654 'total' => $totalBoards,
655 'last_page' => (int) ceil($totalBoards / $perPage)
656 ]
657 ], 200);
658 }
659
660 public function getDashboardViewSettings(Request $request)
661 {
662 $view = $request->getSafe('view', 'sanitize_text_field');
663
664 if ($view == 'kanbanview') {
665 $globalSettings = $this->optionService->getDashboardViewSettings();
666 } elseif ($view == 'listview') {
667 $globalSettings = $this->optionService->getListViewPreferences();
668 } elseif ($view == 'tableview') {
669 $globalSettings = $this->optionService->getTableViewPreferences();
670 } else {
671 // Handle invalid view or default to one
672 return $this->sendError(['message' => __('Invalid view type', 'fluent-boards')], 400);
673 }
674
675 if ($globalSettings->value)
676 $currentSettings = maybe_unserialize($globalSettings->value);
677
678 return $this->sendSuccess([
679 'currentSettings' => $currentSettings,
680 ], 200);
681 }
682
683 public function updateDashboardViewSettings(Request $request)
684 {
685 // updatedSettings is an array, sanitize each element
686 $rawSettings = $request->get('updatedSettings');
687 $newSettings = [];
688 if (is_array($rawSettings)) {
689 foreach ($rawSettings as $key => $value) {
690 $sanitizedKey = sanitize_text_field($key);
691 // Value could be string, boolean, or number - sanitize appropriately
692 if (is_string($value)) {
693 $sanitizedValue = sanitize_text_field($value);
694 } elseif (is_bool($value) || is_numeric($value)) {
695 $sanitizedValue = $value;
696 } else {
697 $sanitizedValue = sanitize_text_field((string)$value);
698 }
699 $newSettings[$sanitizedKey] = $sanitizedValue;
700 }
701 }
702 $view = $request->getSafe('view', 'sanitize_text_field');
703
704 $this->optionService->updateDashboardViewSettings($newSettings, $view);
705
706 if ($view == 'listview') {
707 $message = __("List view settings updated successfully", 'fluent-boards');
708 } elseif ($view == 'tableview') {
709 $message = __("Table view settings updated successfully", 'fluent-boards');
710 } else {
711 $message = __("Card view settings updated successfully", 'fluent-boards');
712 }
713
714 return $this->sendSuccess([
715 'message' => $message,
716 ], 201);
717 }
718
719
720 public function getAddonsSettings()
721 {
722 $canAutoInstallKit = $this->canAutoInstallFluentKit();
723 $kitPluginFile = 'fluent-toolkit/fluent-toolkit.php';
724 $kitLoaded = defined('FLUENT_TOOLKIT_VERSION');
725 $kitPluginExists = $this->isPluginInstalled($kitPluginFile);
726 $kitActionText = __('Get FluentHub from GitHub', 'fluent-boards');
727
728 if ($canAutoInstallKit) {
729 $kitActionText = $kitPluginExists ? __('Activate FluentHub', 'fluent-boards') : __('Install FluentHub', 'fluent-boards');
730 }
731
732 $addOns = [
733 'fluent-crm' => [
734 'title' => __('FluentCRM', 'fluent-boards'),
735 'logo' => fluent_boards_mix('images/addons/fluent-crm.svg'),
736 'is_installed' => defined('FLUENTCRM'),
737 'learn_more_url' => 'https://fluentcrm.com/',
738 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluentcrm/',
739 'action_text' => $this->isPluginInstalled('fluent-crm/fluent-crm.php') ? __('Activate FluentCRM', 'fluent-boards') : __('Install FluentCRM', 'fluent-boards'),
740 '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'),
741 'short_desc' => __('Email marketing automation', 'fluent-boards')
742 ],
743 'fluentform' => [
744 'title' => __('Fluent Forms', 'fluent-boards'),
745 'logo' => fluent_boards_mix('images/addons/fluentform.png'),
746 'is_installed' => defined('FLUENTFORM'),
747 'learn_more_url' => 'https://wordpress.org/plugins/fluentform/',
748 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluent-forms/',
749 'action_text' => $this->isPluginInstalled('fluent-form/fluent-form.php') ? __('Activate Fluent Forms', 'fluent-boards') : __('Install Fluent Forms', 'fluent-boards'),
750 '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'),
751 'short_desc' => __('Create forms and accept payments', 'fluent-boards')
752 ],
753 'fluent-support' => [
754 'title' => __('Fluent Support', 'fluent-boards'),
755 'logo' => fluent_boards_mix('images/addons/fluent-support.svg'),
756 'is_installed' => defined('FLUENT_SUPPORT_VERSION'),
757 'learn_more_url' => 'https://wordpress.org/plugins/fluent-connect/',
758 'settings_url' => admin_url('admin.php?page=fluent-support#/'),
759 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluentsupport/',
760 'action_text' => $this->isPluginInstalled('fluent-support/fluent-support.php') ? __('Activate Fluent Support', 'fluent-boards') : __('Install Fluent Support', 'fluent-boards'),
761 'description' => __('WordPress Helpdesk and Customer Support Ticket Plugin. Provide awesome support and manage customer queries right from your WordPress dashboard.', 'fluent-boards'),
762 'short_desc' => __('Customer support ticketing', 'fluent-boards')
763 ],
764 'fluent-smtp' => [
765 'title' => __('Fluent SMTP', 'fluent-boards'),
766 'logo' => fluent_boards_mix('images/addons/fluent-smtp.svg'),
767 'is_installed' => defined('FLUENTMAIL'),
768 'learn_more_url' => 'https://wordpress.org/plugins/fluent-smtp/',
769 'associate_doc' => admin_url('options-general.php?page=fluent-mail#/'),
770 'action_text' => $this->isPluginInstalled('fluent-smtp/fluent-smtp.php') ? __('Activate Fluent SMTP', 'fluent-boards') : __('Install Fluent SMTP', 'fluent-boards'),
771 'description' => __('The Ultimate SMTP and SES Plugin for WordPress. Connect with any SMTP, SendGrid, Mailgun, SES, Sendinblue, PepiPost, Google, Microsoft and more.', 'fluent-boards'),
772 'short_desc' => __('Reliable email delivery with SMTP', 'fluent-boards')
773 ],
774 'fluent-toolkit' => [
775 'title' => __('FluentHub', 'fluent-boards'),
776 'logo' => fluent_boards_mix('images/addons/fluent-toolkit.svg'),
777 'is_installed' => $kitLoaded,
778 'learn_more_url' => 'https://github.com/WPManageNinja/fluent-toolkit',
779 'settings_url' => admin_url('admin.php?page=fluent-toolkit'),
780 'associate_doc' => 'https://github.com/WPManageNinja/fluent-toolkit',
781 'action_text' => $kitActionText,
782 'install_route' => $canAutoInstallKit ? 'admin/mcp/install-adapter' : '',
783 'install_url' => $canAutoInstallKit ? '' : 'https://github.com/WPManageNinja/fluent-toolkit',
784 'description' => __('Fluent Boards MCP tools become available after FluentHub is installed and active.', 'fluent-boards'),
785 'short_desc' => __('AI agent tools for Fluent Boards', 'fluent-boards')
786 ],
787 ];
788
789 $addOns = apply_filters('fluent_boards/addons_settings', $addOns);
790
791 $modules = fluent_boards_get_pref_settings(false);
792
793 if (empty($modules['frontend']['render_type'])) {
794 $modules['frontend']['render_type'] = 'standalone';
795 }
796
797 $modules['panel_url'] = fluent_boards_page_url();
798
799 return [
800 'addons' => $addOns,
801 'featureModules' => $modules
802 ];
803 }
804
805 public function saveAddonsSettings(Request $request)
806 {
807 if (!defined('FLUENT_BOARDS_PRO')) {
808 return $this->sendError([
809 'message' => __('This feature is only available in Fluent Boards Pro', 'fluent-boards')
810 ]);
811 }
812
813 $rawSettings = $request->get('settings', []);
814
815 // Validate that settings is an array
816 if (!is_array($rawSettings)) {
817 return $this->sendError([
818 'message' => __('Invalid settings format', 'fluent-boards')
819 ], 400);
820 }
821
822 $prefSettings = fluent_boards_get_pref_settings(false);
823
824 $settings = wp_parse_args($rawSettings, $prefSettings);
825
826 $settings = Arr::only($settings, array_keys($prefSettings));
827
828 // Sanitize slug if it exists
829 if (isset($settings['frontend']['slug'])) {
830 $settings['frontend']['slug'] = sanitize_title($settings['frontend']['slug']);
831 }
832
833 if (empty($settings['frontend']['slug'])) {
834 $settings['frontend']['slug'] = 'projects';
835 }
836
837 if (defined('FLUENT_BOARDS_SLUG') && FLUENT_BOARDS_SLUG) {
838 $settings['frontend']['slug'] = FLUENT_BOARDS_SLUG;
839 }
840
841 do_action('fluent_boards/saving_addons', $settings, $prefSettings);
842
843 update_option('fluent_boards_modules', $settings, 'yes');
844
845 if (isset($settings['recurring_task']['enabled']) && $settings['recurring_task']['enabled'] == 'no') {
846 do_action('fluent_boards/recurring_task_disabled');
847 }
848
849 return $this->sendSuccess([
850 'message' => __('Settings are saved', 'fluent-boards'),
851 'featureModules' => $settings
852 ]);
853 }
854
855 public function installPlugin(Request $request)
856 {
857 if (!current_user_can('install_plugins')) {
858 return $this->sendError([
859 'message' => __('Sorry! you do not have permission to install plugin', 'fluent-boards')
860 ]);
861 }
862
863 $plugin = $request->getSafe('plugin', 'sanitize_text_field');
864
865 $acceptedFreePlugins = [
866 'fluent-crm' => 'fluent-crm.php',
867 'fluentform' => 'fluentform.php',
868 'fluent-support' => 'fluent-support.php',
869 'fluent-smtp' => 'fluent-smtp.php'
870 ];
871
872 $acceptedPlugins = apply_filters('fluent_boards/accepted_plugins', $acceptedFreePlugins);
873
874 if (!isset($acceptedPlugins[$plugin])) {
875 return $this->sendError([
876 'message' => __('Invalid plugin', 'fluent-boards')
877 ]);
878 }
879
880 $pluginToInstall = [
881 'name' => __('Fluent Plugin', 'fluent-boards'),
882 'repo-slug' => $plugin,
883 'file' => $acceptedPlugins[$plugin],
884 ];
885
886 // if plugin in free list then run background intaller otherwise call an action to install
887 if (isset($acceptedFreePlugins[$plugin])) {
888 $this->backgroundInstaller($pluginToInstall, $plugin);
889 } else {
890 do_action('fluent_boards/install_plugin', $pluginToInstall, $plugin);
891 }
892
893 return $this->sendSuccess([
894 'message' => __('Plugin is being installed', 'fluent-boards')
895 ]);
896 }
897
898 private function isPluginInstalled($plugin)
899 {
900 return file_exists(WP_PLUGIN_DIR . '/' . $plugin);
901 }
902
903 private function canAutoInstallFluentKit()
904 {
905 $canAutoInstall = (bool) apply_filters('fluent_kit/can_auto_install', false);
906
907 if (!$canAutoInstall) {
908 $canAutoInstall = (bool) apply_filters('fluent_toolkit/can_auto_install', false);
909 }
910
911 return $canAutoInstall;
912 }
913
914 private function backgroundInstaller($plugin_to_install, $plugin_id)
915 {
916 if (!empty($plugin_to_install['repo-slug'])) {
917 require_once ABSPATH . 'wp-admin/includes/file.php';
918 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
919 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
920 require_once ABSPATH . 'wp-admin/includes/plugin.php';
921
922 WP_Filesystem();
923
924 $skin = new \Automatic_Upgrader_Skin();
925 $upgrader = new \WP_Upgrader($skin);
926 $installed_plugins = array_reduce(array_keys(\get_plugins()), array($this, 'associate_plugin_file'), array());
927 $plugin_slug = $plugin_to_install['repo-slug'];
928 $plugin_file = isset($plugin_to_install['file']) ? $plugin_to_install['file'] : $plugin_slug . '.php';
929 $installed = false;
930 $activate = false;
931
932 // See if the plugin is installed already.
933 if (isset($installed_plugins[$plugin_file])) {
934 $installed = true;
935 $activate = !is_plugin_active($installed_plugins[$plugin_file]);
936 }
937
938 // Install this thing!
939 if (!$installed) {
940 // Suppress feedback.
941 ob_start();
942
943 try {
944 $plugin_information = plugins_api(
945 'plugin_information',
946 array(
947 'slug' => $plugin_slug,
948 'fields' => array(
949 'short_description' => false,
950 'sections' => false,
951 'requires' => false,
952 'rating' => false,
953 'ratings' => false,
954 'downloaded' => false,
955 'last_updated' => false,
956 'added' => false,
957 'tags' => false,
958 'homepage' => false,
959 'donate_link' => false,
960 'author_profile' => false,
961 'author' => false,
962 ),
963 )
964 );
965
966 if (is_wp_error($plugin_information)) {
967 throw new \Exception(esc_html($plugin_information->get_error_message()));
968 }
969
970 $package = $plugin_information->download_link;
971 $download = $upgrader->download_package($package);
972
973 if (is_wp_error($download)) {
974 throw new \Exception(esc_html($download->get_error_message()));
975 }
976
977 $working_dir = $upgrader->unpack_package($download, true);
978
979 if (is_wp_error($working_dir)) {
980 throw new \Exception(esc_html($working_dir->get_error_message()));
981 }
982
983 $result = $upgrader->install_package(
984 array(
985 'source' => $working_dir,
986 'destination' => WP_PLUGIN_DIR,
987 'clear_destination' => false,
988 'abort_if_destination_exists' => false,
989 'clear_working' => true,
990 'hook_extra' => array(
991 'type' => 'plugin',
992 'action' => 'install',
993 ),
994 )
995 );
996
997 if (is_wp_error($result)) {
998 throw new \Exception(esc_html($result->get_error_message()));
999 }
1000
1001 $activate = true;
1002
1003 } catch (\Exception $e) {
1004 }
1005
1006 // Discard feedback.
1007 ob_end_clean();
1008 }
1009
1010 wp_clean_plugins_cache();
1011
1012 // Activate this thing.
1013 if ($activate) {
1014 try {
1015 $result = activate_plugin($installed ? $installed_plugins[$plugin_file] : $plugin_slug . '/' . $plugin_file);
1016
1017 if (is_wp_error($result)) {
1018 throw new \Exception(esc_html($result->get_error_message()));
1019 }
1020 } catch (\Exception $e) {
1021 }
1022 }
1023 }
1024 }
1025
1026 private function associate_plugin_file($plugins, $key)
1027 {
1028 $path = explode('/', $key);
1029 $filename = end($path);
1030 $plugins[$filename] = $key;
1031 return $plugins;
1032 }
1033
1034 public function getBoards(Request $request)
1035 {
1036 $boards = Board::select(['id', 'title', 'type'])
1037 ->byAccessUser(get_current_user_id())
1038 ->orderBy('title', 'ASC')
1039 ->get();
1040
1041 return $this->sendSuccess([
1042 'boards' => $boards
1043 ]);
1044 }
1045
1046 public function getPages(Request $request)
1047 {
1048
1049 $db = App::getInstance('db');
1050
1051 $allPages = $db->table('posts')->where('post_type', 'page')
1052 ->where('post_status', 'publish')
1053 ->select(['ID', 'post_title'])
1054 ->orderBy('post_title', 'ASC')
1055 ->get();
1056
1057 $pages = [];
1058 foreach ($allPages as $page) {
1059 $pages[] = [
1060 'id' => $page->ID,
1061 'title' => $page->post_title ? $page->post_title : __('(no title)', 'fluent-boards'),
1062 'url' => esc_url_raw(get_permalink($page->ID))
1063 ];
1064 }
1065
1066 return $this->sendSuccess([
1067 'pages' => $pages
1068 ]);
1069 }
1070
1071 public function getGeneralSettings()
1072 {
1073 $settings = fluent_boards_get_option('general_settings', []);
1074
1075 return $this->sendSuccess([
1076 'settings' => $settings,
1077 'server_timezone' => \wp_timezone_string()
1078 ]);
1079
1080 }
1081
1082 public function saveGeneralSettings(Request $request)
1083 {
1084 // check for pro version
1085 if (!defined('FLUENT_BOARDS_PRO')) {
1086 return $this->sendError([
1087 'message' => __('This feature is only available in Fluent Boards Pro. Please upgrade.', 'fluent-boards')
1088 ]);
1089 }
1090 // updatedSettings is an array, sanitize each element
1091 $rawSettings = $request->get('updatedSettings', []);
1092 $settings = [];
1093 if (is_array($rawSettings)) {
1094 foreach ($rawSettings as $key => $value) {
1095 $sanitizedKey = sanitize_text_field($key);
1096 // Value could be string, boolean, or number - sanitize appropriately
1097 if (is_string($value)) {
1098 $sanitizedValue = sanitize_text_field($value);
1099 } elseif (is_bool($value) || is_numeric($value)) {
1100 $sanitizedValue = $value;
1101 } else {
1102 $sanitizedValue = sanitize_text_field((string)$value);
1103 }
1104 $settings[$sanitizedKey] = $sanitizedValue;
1105 }
1106 }
1107
1108 $settings = apply_filters('fluent_boards/save_general_settings', $settings);
1109
1110 $savedSettings = fluent_boards_update_option('general_settings', $settings);
1111 $savedGeneralSettings = \maybe_unserialize($savedSettings->value);
1112
1113 $scheduleHandler = new ProScheduleHandler();
1114
1115 $dailyReminderEnabled = $savedGeneralSettings['daily_reminder_enabled'] ?? false;
1116
1117 if (filter_var($dailyReminderEnabled, FILTER_VALIDATE_BOOLEAN)) {
1118 // force schedule from this settings update
1119 $scheduleHandler->clearDailyTaskReminderScheduler();
1120 $scheduleHandler->scheduleDailyTaskReminder();
1121 }
1122
1123 return $this->sendSuccess([
1124 'settings' => $savedGeneralSettings,
1125 'message' => __('Settings are saved', 'fluent-boards')
1126 ]);
1127
1128 }
1129
1130 }
1131