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
fluent-boards / app / Http / Controllers / OptionsController.php

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

1,140 lines 45.0 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 ->byAccessUser(get_current_user_id())
81 ->when($search, function ($query) use ($search) {
82 // $search is already sanitized with sanitize_text_field above
83 return $query->where('title', 'LIKE', '%' . $search . '%');
84 })->take(20)->get();
85
86 foreach ($boards as $board) {
87 $options[] = [
88 'id' => $board->id,
89 'title' => $board->title,
90 'left_side_value' => $board->title,
91 'right_side_value' => $board->slug,
92 ];
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
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
122 $users = (new BoardService())->getAssigneesByBoard($boardId, $search);
123 $options = $this->addUserDataAsSelectorOption($users);
124 } else {
125 $options = apply_filters('fluent_boards/ajax_options_' . $optionKey, [], $search, $includedIds);
126 }
127
128 return $this->sendSuccess([
129 'options' => $options,
130 ], 200);
131 } catch (\Exception $e) {
132 return $this->sendError($e->getMessage(), 404);
133 }
134 }
135
136 private function addUserDataAsSelectorOption($users)
137 {
138 $options = [];
139 foreach ($users as $user) {
140 $options[] = [
141 'id' => $user->ID,
142 'email' => $user->user_email,
143 'name' => $user->display_name ?? $user->user_email,
144 'title' => $user->display_name . ' (' . $user->user_email . ')',
145 'photo' => get_avatar_url($user->user_email),
146 ];
147 }
148 return $options;
149 }
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
185 public function getCurrentUserPermissions()
186 {
187 try {
188 $currentUserBoards = Relation::query()
189 ->where('user_id', get_current_user_id())
190 ->whereNotNull('board_id')
191 ->where('status', 'ACTIVE')
192 ->get();
193 foreach ($currentUserBoards as &$currentUserBoardPermission) {
194 $currentUserBoardPermission->permissions = \maybe_unserialize($currentUserBoardPermission->permissions);
195 }
196
197 return $this->sendSuccess(
198 $currentUserBoards, 200
199 );
200 } catch (\Exception $e) {
201 return $this->sendError($e->getMessage(), 404);
202 }
203 }
204
205 public function getUserPermission(Request $request)
206 {
207 try {
208 $boardId = $request->getSafe('boardId', 'intval');
209 $userId = $request->getSafe('userId', 'intval');
210
211 $boardUser = Relation::where('board_id', $boardId)
212 ->where('user_id', $userId)
213 ->where('status', 'ACTIVE')->first();
214 if (!$boardUser->is_admin) {
215 $boardUser->permissions = \maybe_unserialize($boardUser->permissions);
216 }
217
218 return $this->sendSuccess(
219 $boardUser, 200
220 );
221 } catch (\Exception $e) {
222 return $this->sendError($e->getMessage(), 404);
223 }
224 }
225
226 public function updatedUserPermission(Request $request)
227 {
228 try {
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');
233
234 $boardUser = Relation::where('board_id', $boardId)->where('user_id', $userId)->status('ACTIVE')->first();
235
236 if ('Board Admin' == $permission) {
237 $boardUser->is_admin = 'add' == $updateType ? 1 : 0;
238 $boardUser->save();
239 } else {
240 if (0 == $boardUser->is_admin) {
241 $permissionsAlreadyHave = maybe_unserialize($boardUser->permissions);
242 if (!in_array($permission, $permissionsAlreadyHave) && 'add' == $updateType) {
243 array_push($permissionsAlreadyHave, $permission);
244 } elseif (in_array($permission, $permissionsAlreadyHave) && 'remove' == $updateType) {
245 if (($key = array_search($permission, $permissionsAlreadyHave)) !== false) {
246 unset($permissionsAlreadyHave[$key]);
247 }
248 }
249 $boardUser->permissions = serialize($permissionsAlreadyHave);
250 $boardUser->save();
251 }
252 }
253
254 $boardUser->permissions = maybe_unserialize($boardUser->permissions);
255
256 return $this->sendSuccess(
257 $boardUser, 200
258 );
259 } catch (\Exception $e) {
260 return $this->sendError($e->getMessage(), 404);
261 }
262 }
263
264 public function SetUserSuperAdmin($userId)
265 {
266 $userId = absint($userId);
267 try {
268 $this->optionService->createSuperAdmin($userId);
269 return $this->sendSuccess([
270 'message' => __('Member has been set super admin successfully!', 'fluent-boards')
271 ], 200);
272
273 } catch (\Exception $e) {
274 return $this->sendError($e->getMessage(), 404);
275 }
276 }
277
278 public function removeUserSuperAdmin($userId)
279 {
280 $userId = absint($userId);
281 try {
282 $this->optionService->removeUserSuperAdmin($userId);
283
284 return $this->sendSuccess([
285 'message' => __('User has been removed as super admin successfully!', 'fluent-boards'),
286 ], 200);
287 } catch (\Exception $e) {
288 return $this->sendError($e->getMessage(), 404);
289 }
290 }
291
292 public function IsUserAllBoardAdmin(Request $request)
293 {
294 try {
295 $userId = $request->getSafe('id', 'intval');
296 $isSuperAdmin = false;
297 $superAdmin = Relation::where('board_id', null)->where('user_id', $userId)->where('status', 'ACTIVE')->first();
298 $totalSuperAdmin = Relation::where('board_id', null)->where('status', 'ACTIVE')->count();
299 $permissions = [];
300 if ($superAdmin) {
301 $isSuperAdmin = true;
302 $permissions = \maybe_unserialize($superAdmin->permissions);
303 }
304
305 return $this->sendSuccess([
306 'allBoardAdmin' => $isSuperAdmin,
307 'permissions' => $permissions,
308 'numberOfSuperAdmin' => $totalSuperAdmin,
309 ], 200);
310 } catch (\Exception $e) {
311 return $this->sendError($e->getMessage(), 404);
312 }
313 }
314
315 public function RemoveUserFromSuperAdmin(Request $request, $id)
316 {
317 $id = absint($id);
318 try {
319 $userId = $request->getSafe('id', 'intval');
320
321 $superAdmin = Relation::where('board_id', null)->where('user_id', $userId)->first();
322 $superAdmin->status = 'INACTIVE';
323 $superAdmin->save();
324
325 return $this->sendSuccess([
326 'message' => __('User removed as super admin', 'fluent-boards'),
327 ], 201);
328 } catch (\Exception $e) {
329 return $this->sendError($e->getMessage(), 404);
330 }
331 }
332
333 public function removeUserFromBoard(Request $request)
334 {
335 try {
336 $boardId = $request->getSafe('boardId', 'intval');
337 $userId = $request->getSafe('userId', 'intval');
338
339 $this->boardService->removeUserFromBoard($boardId, $userId);
340
341 if (!PermissionManager::isAdmin($userId)) {
342 $this->boardService->removeFromRecentlyOpened($boardId, $userId);
343 }
344
345 return $this->sendSuccess([
346 'message' => __('User Removed from Board successfully!', 'fluent-boards'),
347 ], 201);
348 } catch (\Exception $e) {
349 return $this->sendError($e->getMessage(), 404);
350 }
351 }
352
353 public function addAsSuperAdmin(Request $request)
354 {
355 try {
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 }
362 foreach ($userIds as $userId) {
363 $this->createSuperAdmin($userId);
364 }
365
366 return $this->sendSuccess([
367 'message' => __('Fluent boards admin added', 'fluent-boards'),
368 ], 201);
369 } catch (\Exception $e) {
370 return $this->sendError($e->getMessage(), 404);
371 }
372 }
373
374 private function createSuperAdmin($userId)
375 {
376 try {
377 $existUser = Meta::where('object_id', $userId)->first();
378 if (!$existUser) {
379 $meta = new Meta();
380 $meta->object_id = $userId;
381 $meta->object_type = Constant::FLUENT_BOARD_ADMIN;
382 $meta->save();
383 }
384 } catch (\Exception $e) {
385 return $this->sendError($e->getMessage(), 404);
386 }
387 }
388
389 public function addMembersInBoards(Request $request)
390 {
391 try {
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 }
404
405 foreach ($userIds as $userId) {
406 foreach ($boardIds as $boardId) {
407 $this->boardService->addMembersInBoard($boardId, $userId);
408 }
409 }
410
411 return $this->sendSuccess([
412 'message' => __('Members added to boards', 'fluent-boards'),
413 ], 201);
414 } catch (\Exception $e) {
415 return $this->sendError($e->getMessage(), 404);
416 }
417 }
418
419
420 public function updateGlobalNotificationSettings(Request $request)
421 {
422 try {
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 }
433
434 $this->optionService->updateGlobalNotificationSettings($newSettings);
435
436 return $this->sendSuccess([
437 'message' => __("Notification settings are updated", 'fluent-boards'),
438 ], 201);
439 } catch (\Exception $e) {
440 return $this->sendError($e->getMessage(), 404);
441 }
442 }
443
444 public function getGlobalNotificationSettings()
445 {
446 try {
447 $globalSettings = $this->optionService->getGlobalNotificationSettings();
448 if ($globalSettings->value)
449 $currentSettings = maybe_unserialize($globalSettings->value);
450
451 return $this->sendSuccess([
452 'currentSettings' => $currentSettings,
453 ], 200);
454 } catch (\Exception $e) {
455 return $this->sendError($e->getMessage(), 404);
456 }
457 }
458
459 public function getBoardMembers(Request $request)
460 {
461 if (!PermissionManager::userHasAnyBoardAccess()) {
462 return $this->sendError([
463 'message' => __('You do not have permission to access this route', 'fluent-boards')
464 ]);
465 }
466
467 $boardId = $request->getSafe('boardId', 'intval');
468
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)
476 ->select(['foreign_id'])
477 ->groupBy('foreign_id');
478
479 if ($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);
484 }
485
486 $memberUserIds = $memberUserIdsQuery->get()
487 ->pluck('foreign_id')->toArray();
488
489 $members = [];
490
491 if ($memberUserIds) {
492 $memberUsers = get_users([
493 'include' => $memberUserIds
494 ]);
495
496
497 foreach ($memberUsers as $memberUser) {
498 $name = trim($memberUser->first_name . ' ' . $memberUser->last_name);
499 if (!$name) {
500 $name = $memberUser->display_name;
501 }
502
503 $members[$memberUser->ID] = [
504 'ID' => $memberUser->ID,
505 'display_name' => $name,
506 'photo' => get_avatar_url($memberUser->user_email)
507 ];
508 }
509
510 }
511
512 $adminUsers = get_users([
513 'role' => 'administrator',
514 'exclude' => $memberUserIds
515 ]);
516
517 foreach ($adminUsers as $user) {
518 $name = trim($user->first_name . ' ' . $user->last_name);
519 if (!$name) {
520 $name = $user->display_name;
521 }
522 $members[$user->ID] = [
523 'ID' => $user->ID,
524 'display_name' => $name,
525 'photo' => get_avatar_url($user->user_email)
526 ];
527 }
528
529 $members = array_values($members);
530
531 // sort members by name
532 usort($members, function ($a, $b) {
533 return strcmp($a['display_name'], $b['display_name']);
534 });
535
536 return [
537 'members' => $members
538 ];
539 }
540
541 public function globalSearch()
542 {
543 $currentUserId = get_current_user_id();
544
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';
549
550 // Pagination parameters
551 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
552 $taskPage = isset($_REQUEST['task_page']) ? max(1, (int)$_REQUEST['task_page']) : 0;
553 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
554 $boardPage = isset($_REQUEST['board_page']) ? max(1, (int)$_REQUEST['board_page']) : 0;
555 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API
556 $perPage = isset($_REQUEST['per_page']) ? (int)$_REQUEST['per_page'] : 20;
557 $perPage = max(1, min(100, $perPage));
558
559 // Build base queries
560 $firstThreeChars = substr($query, 0, 3);
561 $firstNineChars = substr($query, 0, 9);
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);
578 if ($scope == 'all') {
579 if (!$isUserAdmin) {
580 $boardIds = PermissionManager::getBoardIdsForUser($currentUserId);
581 $boardQuery->whereIn('id', $boardIds);
582 $tasksQuery->whereIn('board_id', $boardIds);
583 }
584 } else {
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);
591 } else {
592 $tasksQuery->where('id', -1); // Force no results
593 }
594 }
595
596 $allActiveBoardsIds = Board::query()->where('archived_at', null)->pluck('id')->toArray();
597
598 $boards = [];
599 $tasks = [];
600 $totalBoards = 0;
601 $totalTasks = 0;
602 $formattedBoards = [];
603 $formattedTasks = [];
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
619 foreach ($boards as $board) {
620 $formattedBoards[] = [
621 'type' => 'board',
622 'id' => $board->id,
623 'title' => $board->title,
624 'description' => DescriptionMarkdownConverter::normalize($board->description),
625 ];
626 }
627 foreach ($tasks as $task) {
628 if (!in_array($task->board_id, $allActiveBoardsIds)) {
629 continue;
630 }
631
632 $board = $task->board;
633 $formattedTasks[] = [
634 'type' => 'task',
635 'id' => $task->id,
636 'title' => $task->title,
637 'description' => DescriptionMarkdownConverter::normalize($task->description),
638 'board_id' => $task->board_id,
639 'board' => [
640 'id' => $board->id,
641 'title' => $board->title,
642 'url' => Helper::getBoardUrl($board->id)
643 ],
644 'stage' => [
645 'id' => $task->stage_id,
646 'title' => $task->stage->title ?? '',
647 ],
648 ];
649 }
650
651 return $this->sendSuccess([
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 ]
666 ], 200);
667 }
668
669 public function getDashboardViewSettings(Request $request)
670 {
671 $view = $request->getSafe('view', 'sanitize_text_field');
672
673 if ($view == 'kanbanview') {
674 $globalSettings = $this->optionService->getDashboardViewSettings();
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 }
683
684 if ($globalSettings->value)
685 $currentSettings = maybe_unserialize($globalSettings->value);
686
687 return $this->sendSuccess([
688 'currentSettings' => $currentSettings,
689 ], 200);
690 }
691
692 public function updateDashboardViewSettings(Request $request)
693 {
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');
712
713 $this->optionService->updateDashboardViewSettings($newSettings, $view);
714
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');
721 }
722
723 return $this->sendSuccess([
724 'message' => $message,
725 ], 201);
726 }
727
728
729 public function getAddonsSettings()
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
741 $addOns = [
742 'fluent-crm' => [
743 'title' => __('FluentCRM', 'fluent-boards'),
744 'logo' => fluent_boards_mix('images/addons/fluent-crm.svg'),
745 'is_installed' => defined('FLUENTCRM'),
746 'learn_more_url' => 'https://fluentcrm.com/',
747 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluentcrm/',
748 'action_text' => $this->isPluginInstalled('fluent-crm/fluent-crm.php') ? __('Activate FluentCRM', 'fluent-boards') : __('Install FluentCRM', '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')
751 ],
752 'fluentform' => [
753 'title' => __('Fluent Forms', 'fluent-boards'),
754 'logo' => fluent_boards_mix('images/addons/fluentform.png'),
755 'is_installed' => defined('FLUENTFORM'),
756 'learn_more_url' => 'https://wordpress.org/plugins/fluentform/',
757 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluent-forms/',
758 'action_text' => $this->isPluginInstalled('fluent-form/fluent-form.php') ? __('Activate Fluent Forms', 'fluent-boards') : __('Install Fluent Forms', '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')
761 ],
762 'fluent-support' => [
763 'title' => __('Fluent Support', 'fluent-boards'),
764 'logo' => fluent_boards_mix('images/addons/fluent-support.svg'),
765 'is_installed' => defined('FLUENT_SUPPORT_VERSION'),
766 'learn_more_url' => 'https://wordpress.org/plugins/fluent-connect/',
767 'settings_url' => admin_url('admin.php?page=fluent-support#/'),
768 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluentsupport/',
769 'action_text' => $this->isPluginInstalled('fluent-support/fluent-support.php') ? __('Activate Fluent Support', 'fluent-boards') : __('Install Fluent Support', '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')
772 ],
773 'fluent-smtp' => [
774 'title' => __('Fluent SMTP', 'fluent-boards'),
775 'logo' => fluent_boards_mix('images/addons/fluent-smtp.svg'),
776 'is_installed' => defined('FLUENTMAIL'),
777 'learn_more_url' => 'https://wordpress.org/plugins/fluent-smtp/',
778 'associate_doc' => admin_url('options-general.php?page=fluent-mail#/'),
779 'action_text' => $this->isPluginInstalled('fluent-smtp/fluent-smtp.php') ? __('Activate Fluent SMTP', 'fluent-boards') : __('Install Fluent SMTP', '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')
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 ],
796 ];
797
798 $addOns = apply_filters('fluent_boards/addons_settings', $addOns);
799
800 $modules = fluent_boards_get_pref_settings(false);
801
802 if (empty($modules['frontend']['render_type'])) {
803 $modules['frontend']['render_type'] = 'standalone';
804 }
805
806 $modules['panel_url'] = fluent_boards_page_url();
807
808 return [
809 'addons' => $addOns,
810 'featureModules' => $modules
811 ];
812 }
813
814 public function saveAddonsSettings(Request $request)
815 {
816 if (!defined('FLUENT_BOARDS_PRO')) {
817 return $this->sendError([
818 'message' => __('This feature is only available in Fluent Boards Pro', 'fluent-boards')
819 ]);
820 }
821
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 }
830
831 $prefSettings = fluent_boards_get_pref_settings(false);
832
833 $settings = wp_parse_args($rawSettings, $prefSettings);
834
835 $settings = Arr::only($settings, array_keys($prefSettings));
836
837 // Sanitize slug if it exists
838 if (isset($settings['frontend']['slug'])) {
839 $settings['frontend']['slug'] = sanitize_title($settings['frontend']['slug']);
840 }
841
842 if (empty($settings['frontend']['slug'])) {
843 $settings['frontend']['slug'] = 'projects';
844 }
845
846 if (defined('FLUENT_BOARDS_SLUG') && FLUENT_BOARDS_SLUG) {
847 $settings['frontend']['slug'] = FLUENT_BOARDS_SLUG;
848 }
849
850 do_action('fluent_boards/saving_addons', $settings, $prefSettings);
851
852 update_option('fluent_boards_modules', $settings, 'yes');
853
854 if (isset($settings['recurring_task']['enabled']) && $settings['recurring_task']['enabled'] == 'no') {
855 do_action('fluent_boards/recurring_task_disabled');
856 }
857
858 return $this->sendSuccess([
859 'message' => __('Settings are saved', 'fluent-boards'),
860 'featureModules' => $settings
861 ]);
862 }
863
864 public function installPlugin(Request $request)
865 {
866 if (!current_user_can('install_plugins')) {
867 return $this->sendError([
868 'message' => __('Sorry! you do not have permission to install plugin', 'fluent-boards')
869 ]);
870 }
871
872 $plugin = $request->getSafe('plugin', 'sanitize_text_field');
873
874 $acceptedFreePlugins = [
875 'fluent-crm' => 'fluent-crm.php',
876 'fluentform' => 'fluentform.php',
877 'fluent-support' => 'fluent-support.php',
878 'fluent-smtp' => 'fluent-smtp.php'
879 ];
880
881 $acceptedPlugins = apply_filters('fluent_boards/accepted_plugins', $acceptedFreePlugins);
882
883 if (!isset($acceptedPlugins[$plugin])) {
884 return $this->sendError([
885 'message' => __('Invalid plugin', 'fluent-boards')
886 ]);
887 }
888
889 $pluginToInstall = [
890 'name' => __('Fluent Plugin', 'fluent-boards'),
891 'repo-slug' => $plugin,
892 'file' => $acceptedPlugins[$plugin],
893 ];
894
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 }
901
902 return $this->sendSuccess([
903 'message' => __('Plugin is being installed', 'fluent-boards')
904 ]);
905 }
906
907 private function isPluginInstalled($plugin)
908 {
909 return file_exists(WP_PLUGIN_DIR . '/' . $plugin);
910 }
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
923 private function backgroundInstaller($plugin_to_install, $plugin_id)
924 {
925 if (!empty($plugin_to_install['repo-slug'])) {
926 require_once ABSPATH . 'wp-admin/includes/file.php';
927 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
928 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
929 require_once ABSPATH . 'wp-admin/includes/plugin.php';
930
931 WP_Filesystem();
932
933 $skin = new \Automatic_Upgrader_Skin();
934 $upgrader = new \WP_Upgrader($skin);
935 $installed_plugins = array_reduce(array_keys(\get_plugins()), array($this, 'associate_plugin_file'), array());
936 $plugin_slug = $plugin_to_install['repo-slug'];
937 $plugin_file = isset($plugin_to_install['file']) ? $plugin_to_install['file'] : $plugin_slug . '.php';
938 $installed = false;
939 $activate = false;
940
941 // See if the plugin is installed already.
942 if (isset($installed_plugins[$plugin_file])) {
943 $installed = true;
944 $activate = !is_plugin_active($installed_plugins[$plugin_file]);
945 }
946
947 // Install this thing!
948 if (!$installed) {
949 // Suppress feedback.
950 ob_start();
951
952 try {
953 $plugin_information = plugins_api(
954 'plugin_information',
955 array(
956 'slug' => $plugin_slug,
957 'fields' => array(
958 'short_description' => false,
959 'sections' => false,
960 'requires' => false,
961 'rating' => false,
962 'ratings' => false,
963 'downloaded' => false,
964 'last_updated' => false,
965 'added' => false,
966 'tags' => false,
967 'homepage' => false,
968 'donate_link' => false,
969 'author_profile' => false,
970 'author' => false,
971 ),
972 )
973 );
974
975 if (is_wp_error($plugin_information)) {
976 throw new \Exception(esc_html($plugin_information->get_error_message()));
977 }
978
979 $package = $plugin_information->download_link;
980 $download = $upgrader->download_package($package);
981
982 if (is_wp_error($download)) {
983 throw new \Exception(esc_html($download->get_error_message()));
984 }
985
986 $working_dir = $upgrader->unpack_package($download, true);
987
988 if (is_wp_error($working_dir)) {
989 throw new \Exception(esc_html($working_dir->get_error_message()));
990 }
991
992 $result = $upgrader->install_package(
993 array(
994 'source' => $working_dir,
995 'destination' => WP_PLUGIN_DIR,
996 'clear_destination' => false,
997 'abort_if_destination_exists' => false,
998 'clear_working' => true,
999 'hook_extra' => array(
1000 'type' => 'plugin',
1001 'action' => 'install',
1002 ),
1003 )
1004 );
1005
1006 if (is_wp_error($result)) {
1007 throw new \Exception(esc_html($result->get_error_message()));
1008 }
1009
1010 $activate = true;
1011
1012 } catch (\Exception $e) {
1013 }
1014
1015 // Discard feedback.
1016 ob_end_clean();
1017 }
1018
1019 wp_clean_plugins_cache();
1020
1021 // Activate this thing.
1022 if ($activate) {
1023 try {
1024 $result = activate_plugin($installed ? $installed_plugins[$plugin_file] : $plugin_slug . '/' . $plugin_file);
1025
1026 if (is_wp_error($result)) {
1027 throw new \Exception(esc_html($result->get_error_message()));
1028 }
1029 } catch (\Exception $e) {
1030 }
1031 }
1032 }
1033 }
1034
1035 private function associate_plugin_file($plugins, $key)
1036 {
1037 $path = explode('/', $key);
1038 $filename = end($path);
1039 $plugins[$filename] = $key;
1040 return $plugins;
1041 }
1042
1043 public function getBoards(Request $request)
1044 {
1045 $boards = Board::select(['id', 'title', 'type'])
1046 ->byAccessUser(get_current_user_id())
1047 ->orderBy('title', 'ASC')
1048 ->get();
1049
1050 return $this->sendSuccess([
1051 'boards' => $boards
1052 ]);
1053 }
1054
1055 public function getPages(Request $request)
1056 {
1057
1058 $db = App::getInstance('db');
1059
1060 $allPages = $db->table('posts')->where('post_type', 'page')
1061 ->where('post_status', 'publish')
1062 ->select(['ID', 'post_title'])
1063 ->orderBy('post_title', 'ASC')
1064 ->get();
1065
1066 $pages = [];
1067 foreach ($allPages as $page) {
1068 $pages[] = [
1069 'id' => $page->ID,
1070 'title' => $page->post_title ? $page->post_title : __('(no title)', 'fluent-boards'),
1071 'url' => esc_url_raw(get_permalink($page->ID))
1072 ];
1073 }
1074
1075 return $this->sendSuccess([
1076 'pages' => $pages
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
1137 }
1138
1139 }
1140