PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.2.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.2.0
2.2.0 2.1.0 2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 1.40 All 43 releases
fluent-boards / app / Http / Controllers / OptionsController.php

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

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