PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 1.95.2
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v1.95.2
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 1.95.2, at app/Http/Controllers/OptionsController.php

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