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

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

1,483 lines 50.7 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\Models\Attachment;
6 use FluentBoards\App\Models\Meta;
7 use FluentBoards\App\Models\Relation;
8 use FluentBoards\App\Models\Task;
9 use FluentBoards\App\Models\User;
10 use FluentBoards\App\Models\Board;
11 use FluentBoards\App\Services\CommentService;
12 use FluentBoards\App\Services\Constant;
13 use FluentBoards\App\Services\DescriptionMarkdownConverter;
14 use FluentBoards\App\Services\Helper;
15 use FluentBoards\App\Models\Stage;
16 use FluentBoards\App\Services\InstallService;
17 use FluentBoards\App\Services\StageService;
18 use FluentBoards\App\Services\TaskService;
19 use FluentBoards\App\Services\BoardService;
20 use FluentBoards\App\Services\FolderService;
21 use FluentBoards\App\Services\UploadService;
22 use FluentBoards\Framework\Http\Request\Request;
23 use FluentBoards\App\Services\PermissionManager;
24 use FluentBoards\App\Services\PublicAccessService;
25 use FluentBoards\App\Hooks\Handlers\BoardHandler;
26 use FluentBoards\App\Hooks\Handlers\BoardMenuHandler;
27 use FluentBoards\App\Services\LabelService;
28 use FluentBoards\Framework\Support\Arr;
29 use FluentBoards\Framework\Support\Collection;
30 use FluentBoardsPro\App\Services\AttachmentService;
31 use FluentBoardsPro\App\Services\CustomFieldService;
32 use FluentBoardsPro\App\Services\RemoteUrlParser;
33
34 class BoardController extends Controller
35 {
36 private const LEGACY_BOARD_ASSOCIATED_CRM_CONTACT = 'crm_contact';
37
38 private $boardService;
39 private $taskService;
40 private $stageService;
41 private $labelService;
42
43 public function __construct(
44 BoardService $boardService,
45 TaskService $taskService,
46 StageService $stageService,
47 LabelService $labelService
48 )
49 {
50 parent::__construct();
51 $this->boardService = $boardService;
52 $this->taskService = $taskService;
53 $this->stageService = $stageService;
54 $this->labelService = $labelService;
55 }
56
57 public function getBoards(Request $request)
58 {
59 $per_page = $request->getSafe('per_page', 'intval', 100);
60 $per_page = max(1, min(100, $per_page));
61 $userId = get_current_user_id();
62 $type = $request->getSafe('type', 'sanitize_text_field', 'to-do');
63
64 $order = $request->getSafe('order', 'sanitize_text_field', 'created_at');
65 $orderBy = $request->getSafe('orderBy', 'sanitize_text_field', 'DESC');
66 $searchInput = $request->getSafe('searchInput', 'sanitize_text_field');
67
68 $option = $request->getSafe('option', 'sanitize_text_field');
69 $folderId = $request->getSafe('fid', 'intval'); // Get folder ID from request
70
71 // Initialize the query based on archive status
72 if (!defined('FLUENT_ROADMAP')) {
73 if ($option == 'archived') {
74 $relatedBoardsQuery = Board::whereNotNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
75 } else {
76 $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
77 }
78 } else {
79 if ($option == 'archived') {
80 $relatedBoardsQuery = Board::whereNotNull('archived_at')->byAccessUser($userId);
81 } else {
82 $relatedBoardsQuery = Board::whereNull('archived_at')->byAccessUser($userId);
83 }
84 }
85
86 // Scope pinned before pagination, from the same id source as getBoardCounts(),
87 // so the pinned page and board_counts.pinned always agree. Filtering pinned
88 // after pagination would drop pinned boards that fall on a later active page.
89 if ($option == 'pinned') {
90 $pinnedIds = $this->boardService->getPinnedBoardIds();
91
92 $relatedBoardsQuery = $pinnedIds
93 ? $relatedBoardsQuery->whereIn('id', $pinnedIds)
94 : $relatedBoardsQuery->where('id', 0);
95 }
96
97 if ($folderId) {
98 $boardIds = (new FolderService())->getBoardIdsByFolder($folderId);
99 $relatedBoardsQuery = $boardIds
100 ? $relatedBoardsQuery->whereIn('id', $boardIds)
101 : $relatedBoardsQuery->where('id', 0);
102 }
103
104 // Filter out boards that are templates (exclude boards where settings->is_template is true)
105 $relatedBoardsQuery = $relatedBoardsQuery->excludeTemplates();
106
107 // Add search functionality
108 if (!empty($searchInput)) {
109 $relatedBoardsQuery = $relatedBoardsQuery->where('title', 'like', '%' . $searchInput . '%');
110 }
111
112 $relatedBoards = $relatedBoardsQuery->orderBy($order, $orderBy)
113 ->withCount('completedTasks')
114 ->with('stages', 'users')
115 ->paginate($per_page);
116
117 foreach ($relatedBoards as $relatedBoard) {
118 $relatedBoard->description = DescriptionMarkdownConverter::normalize($relatedBoard->description);
119 $relatedBoard->users = Helper::sanitizeUserCollections($relatedBoard->users);
120 $relatedBoard->is_pinned = $this->boardService->isPinned($relatedBoard->id);
121 }
122
123 $response = [
124 'boards' => $relatedBoards,
125 'board_counts' => $this->boardService->getBoardCounts($userId)
126 ];
127
128 $folderMapping = $this->getBoardFolderMapping($userId);
129 $response['folder_mapping'] = $folderMapping;
130 if ($folderId) {
131 $response['current_folder'] = isset($folderMapping[$folderId])
132 ? $this->getCurrentFolderInfoFromMapping($folderMapping[$folderId])
133 : null;
134 }
135
136 return $this->sendSuccess($response);
137 }
138
139 /**
140 * Get folder mapping for boards
141 */
142 private function getBoardFolderMapping($userId)
143 {
144 $folderService = new FolderService();
145 $folders = $folderService->getFolders($userId);
146
147 $mapping = [];
148 foreach ($folders as $folder) {
149 $mapping[$folder->id] = [
150 'id' => $folder->id,
151 'title' => $folder->title,
152 'board_ids' => $folder->boards ? $folder->boards->pluck('id')->toArray() : []
153 ];
154 }
155
156 return $mapping;
157 }
158
159 private function getCurrentFolderInfoFromMapping(array $folder)
160 {
161 return [
162 'id' => $folder['id'],
163 'title' => $folder['title'],
164 'board_count' => count($folder['board_ids'])
165 ];
166 }
167
168 /**
169 * Get the list of boards and their associated stages for the current user.
170 *
171 * @param \FluentBoards\Framework\Http\Request\Request $request
172 * @return \WP_REST_Response
173 */
174 public function getBoardsList(Request $request)
175 {
176 $userId = get_current_user_id();
177
178 // Query to fetch boards that are not archived and accessible by the user
179 // Check if the FLUENT_ROADMAP constant is defined
180 if (!defined('FLUENT_ROADMAP')) {
181 $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->excludeTemplates()->byAccessUser($userId);
182 } else {
183 $relatedBoardsQuery = Board::whereNull('archived_at')->excludeTemplates()->byAccessUser($userId);
184 }
185
186 $relatedBoards = $relatedBoardsQuery->with('stages')->get();
187
188 // Fetch the stages associated with the boards
189 $stages = Stage::whereIn('board_id', $relatedBoards->pluck('id'))->where('archived_at', null)->get();
190
191 return $this->sendSuccess([
192 'boards' => $relatedBoards,
193 'all_stages' => $stages,
194 ], 200);
195 }
196 public function getOnlyBoardsByUser(Request $request)
197 {
198 try {
199 $userId = get_current_user_id();
200
201 $searchInput = $request->getSafe('searchInput', 'sanitize_text_field');
202
203
204 if(!defined('FLUENT_ROADMAP'))
205 {
206 $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->excludeTemplates()->byAccessUser($userId);
207 } else {
208 $relatedBoardsQuery = Board::whereNull('archived_at')->excludeTemplates()->byAccessUser($userId);
209 }
210
211 if (!empty($searchInput)) {
212 $relatedBoardsQuery = $relatedBoardsQuery->where('title', 'like', '%' . $searchInput . '%');
213 }
214
215 $relatedBoards = $relatedBoardsQuery->orderBy('created_at', 'DESC')->get();
216
217 return $this->sendSuccess([
218 'boards' => $relatedBoards
219 ]);
220 } catch (\Exception $e) {
221 return $this->sendError([
222 'message' => $e->getMessage()
223 ]);
224 }
225 }
226
227 public function getRecentBoards()
228 {
229 $boards = $this->boardService->getRecentBoards();
230
231 if (!$boards || $boards->isEmpty()) {
232 $boards = Board::whereNull('archived_at')
233 ->excludeTemplates()
234 ->availableInCurrentInstall()
235 ->byAccessUser(get_current_user_id())
236 ->limit(4)
237 ->withCount('completedTasks')
238 ->with(['stages', 'users'])
239 ->get();
240 }
241
242 foreach ($boards as $board) {
243 $board->users = Helper::sanitizeUserCollections($board->users);
244 $board->is_pinned = $this->boardService->isPinned($board->id);
245 }
246
247 return [
248 'boards' => $boards,
249 ];
250 }
251
252 /*
253 * TODO: Refactor this method , remove this
254 */
255 public function getBoardsByType($type)
256 {
257 $boards = $this->boardService->getBoardsByType($type);
258
259 return $this->sendSuccess([
260 'boards' => $boards,
261 ]);
262 }
263
264 public function createFirstBoard(Request $request)
265 {
266 $boardData = $this->boardSanitizeAndValidate($request->get('board'), [
267 'title' => 'required|string',
268 'description' => 'nullable',
269 'type' => 'required|string',
270 'currency' => 'nullable|string',
271 'crm_contact_id' => 'nullable|numeric',
272 ]);
273
274 $installFluentCRM = $request->getSafe('withFluentCRM', 'sanitize_text_field') == 'yes' ? true : false;
275
276 $postStages = $request->get('stages');
277 if (!is_array($postStages)) {
278 $postStages = [];
279 }
280 $stageData = array();
281 foreach ($postStages as $stage) {
282 $temp = $this->stageSanitizeAndValidate($stage, [
283 'title' => 'required|string',
284 ]);
285 $stageData[] = $temp;
286 }
287
288 $taskData = null;
289 if ($request->get('task')) {
290 $taskData = $this->taskSanitizeAndValidate($request->get('task'), [
291 'title' => 'required|string',
292 ]);
293 }
294
295 $board = $this->boardService->createBoard($boardData);
296 $this->labelService->createDefaultLabel($board->id);
297 $type = ucfirst($boardData['type']);
298 $stage = $this->stageService->createStages($board, $stageData);
299
300 if ($taskData) {
301 $taskData['board_id'] = $board->id;
302 $taskData['stage_id'] = $stage->id;
303 $this->taskService->createTask($taskData, $board->id);
304 }
305
306 do_action('fluent_boards/board_created', $board);
307
308 if ($installFluentCRM && !defined('FLUENTCRM')) {
309 InstallService::install('fluent-crm');
310 }
311
312 return [
313 'message' => __('Board has been created', 'fluent-boards'),
314 'board' => $board,
315 ];
316 }
317
318 public function skipOnboarding(Request $request)
319 {
320 $onboarding = Meta::where('key', Constant::FBS_ONBOARDING)->first();
321 if($onboarding && $onboarding->value == 'no'){
322 $onboarding->value = 'yes' ;
323 $onboarding->save();
324 }
325
326 return [
327 'message' => __('Onboarding skipped successfully', 'fluent-boards'),
328 ];
329 }
330
331 public function create(Request $request)
332 {
333 $boardData = $this->boardSanitizeAndValidate($request->get('board'), [
334 'title' => 'required|string',
335 'description' => 'nullable',
336 'type' => 'required|string',
337 'currency' => 'nullable|string',
338 'crm_contact_id' => 'nullable|numeric',
339 'folder_id' => 'nullable',
340 ]);
341
342 try {
343 $folderId = $request->getSafe('folder_id', 'intval');
344 $folderService = new FolderService();
345 if ($folderId) {
346 $folderService->assertCanModifyFolder($folderId);
347 }
348
349 $backgroundData = $this->sanitizeCreateBoardBackground($request->get('background'));
350 if (!empty($backgroundData)) {
351 $boardData['background'] = $backgroundData;
352 }
353
354 $board = $this->boardService->createBoard($boardData);
355 $this->createBoardLabelsFromRequest($request, $board->id);
356 $this->addBoardMembersFromRequest($request, $board->id);
357 $type = ucfirst($boardData['type']);
358 $stages = $request->get('stages');
359 $sanitizedStages = [];
360
361 if (is_array($stages) && !empty($stages)) {
362 foreach ($stages as $stage) {
363 $sanitizedStages[] = $this->stageSanitizeAndValidate($stage, [
364 'title' => 'required|string',
365 'slug' => 'nullable|string',
366 'position' => 'nullable|numeric'
367 ]);
368 }
369 }
370
371 if (isset($boardData['type']) && $boardData['type'] == 'roadmap') {
372 $this->stageService->createRoadmapStages($board, $sanitizedStages);
373 } elseif (!empty($sanitizedStages)) {
374 $this->stageService->createStages($board, $sanitizedStages);
375 } else {
376 $this->stageService->createDefaultStages($board);
377 }
378
379 // if board is created from crm contact
380 if (isset($boardData['crm_contact_id'])) {
381 $this->boardService->updateAssociateMember($boardData['crm_contact_id'], $board->id);
382 }
383
384 do_action('fluent_boards/board_created', $board);
385
386
387 if ($folderId) {
388 $folderService->addBoardToFolder($folderId, [$board->id]);
389 }
390
391 $message = __('Board has been created successfully', 'fluent-boards');
392
393 return $this->send([
394 'message' => $message,
395 'board' => $board,
396 ], 201);
397 } catch (\Exception $e) {
398 return $this->sendError([
399 'message' => $e->getMessage()
400 ]);
401 }
402 }
403
404 private function sanitizeCreateBoardBackground($background)
405 {
406 if (!is_array($background) || empty($background['id'])) {
407 return '';
408 }
409
410 $backgroundId = sanitize_text_field($background['id']);
411
412 // Only accept ids from the curated solid/gradient palettes and always
413 // persist the canonical value from the constant (never the client-supplied
414 // color) so arbitrary CSS can't be stored and later rendered into a style.
415 $allowedBackgrounds = [];
416 foreach (array_merge(
417 Constant::BOARD_BACKGROUND_DEFAULT_SOLID_COLORS,
418 Constant::BOARD_BACKGROUND_DEFAULT_GRADIENT_COLORS
419 ) as $option) {
420 if (isset($option['id'], $option['value'])) {
421 $allowedBackgrounds[$option['id']] = $option['value'];
422 }
423 }
424
425 if (!isset($allowedBackgrounds[$backgroundId])) {
426 return '';
427 }
428
429 return [
430 'id' => $backgroundId,
431 'color' => $allowedBackgrounds[$backgroundId],
432 'is_image' => false,
433 'image_url' => null,
434 ];
435 }
436
437 private function createBoardLabelsFromRequest(Request $request, $boardId)
438 {
439 $labels = $request->get('labels');
440
441 if (!is_array($labels)) {
442 $this->labelService->createDefaultLabel($boardId);
443 return;
444 }
445
446 foreach ($labels as $label) {
447 $labelData = Helper::sanitizeLabel((array) $label);
448
449 if (empty($labelData['label']) && empty($labelData['bg_color'])) {
450 continue;
451 }
452
453 $this->labelService->createLabel([
454 'label' => $labelData['label'] ?? '',
455 'bg_color' => $labelData['bg_color'] ?? '#f3f4f6',
456 'color' => $labelData['color'] ?? '#1B2533',
457 ], $boardId);
458 }
459 }
460
461 private function addBoardMembersFromRequest(Request $request, $boardId)
462 {
463 $memberIds = $request->get('member_ids');
464
465 if (!is_array($memberIds)) {
466 return;
467 }
468
469 $memberIds = array_filter(array_unique(array_map('intval', $memberIds)));
470 $currentUserId = get_current_user_id();
471
472 foreach ($memberIds as $memberId) {
473 if ($memberId === $currentUserId) {
474 continue;
475 }
476
477 $this->boardService->addMembersInBoard($boardId, $memberId);
478 }
479 }
480
481 /**
482 * Get archived stages for a board with optional pagination and archive actor metadata.
483 */
484 public function getArchivedStage(Request $request, $board_id)
485 {
486 try {
487 $board_id = absint($board_id);
488 $sanitizedParams = [
489 'noPagination' => $request->getSafe('noPagination', 'boolval', false),
490 'per_page' => $request->getSafe('per_page', 'intval', 30),
491 'page' => $request->getSafe('page', 'intval', 1),
492 ];
493
494 $stages = $this->stageService->getArchivedStages($sanitizedParams, $board_id);
495
496 return $this->sendSuccess([
497 'stages' => $stages,
498 ], 200);
499 } catch (\Exception $e) {
500 return $this->sendError($e->getMessage(), 404);
501 }
502 }
503
504 public function find(Request $request, $board_id)
505 {
506 $board = Board::findOrFail($board_id);
507 $board->description = DescriptionMarkdownConverter::normalize($board->description);
508 $includeArchived = filter_var($request->get('include_archived', false), FILTER_VALIDATE_BOOLEAN);
509 $board->background = maybe_unserialize($board->background);
510 $board->createdOn = $board->created_at->format('Y-m-d');
511
512 $board->load(['users', 'labels', 'owner']);
513
514 if ($includeArchived) {
515 $board->stages = Stage::where('board_id', $board_id)
516 ->orderBy('position', 'asc')
517 ->get();
518 } else {
519 $board->load('stages');
520 }
521
522 if (defined('FLUENT_BOARDS_PRO')){
523 $customFiledPositionMeta = $board->getMetaByKey('custom_field_positions');
524 if(!$customFiledPositionMeta) {
525 (new CustomFieldService())->reIndexCustomFieldPositions($board_id);
526 $board->updateMeta('custom_field_positions', 'yes');
527 }
528
529 $board->load(['customFields']);
530 }
531
532 $this->boardService->updateRecentBoards($board_id);
533
534 $board->labelColor = Constant::TRELLO_COLOR_MAP;
535 $board->labelColorText = Constant::TEXT_COLOR_MAP;
536
537 $board->users = Helper::sanitizeUserCollections($board->users);
538 $board->owner = Helper::sanitizeUserCollections($board->owner);
539
540 $board->is_pinned = $this->boardService->isPinned($board->id);
541
542 $board = apply_filters('fluent_boards/board_find', $board);
543
544 return [
545 'board' => $board,
546 'synced_at' => current_time('mysql')
547 ];
548 }
549
550 public function update(Request $request, $board_id)
551 {
552 $boardData = $this->boardSanitizeAndValidate($request->only(['title', 'description']), [
553 'title' => 'required|string',
554 'description' => 'nullable|string',
555 ]);
556
557 $board = Board::findOrFail($board_id);
558 $boardData['description'] = DescriptionMarkdownConverter::normalize($boardData['description']);
559
560 $oldBoard = clone $board;
561 $board->fill($boardData);
562 $board->save();
563
564 do_action('fluent_boards/board_updated', $board, $oldBoard);
565
566 return [
567 'message' => __('Board has been updated', 'fluent-boards'),
568 'board' => $board,
569 'stages' => $board->stages()->get(),
570 ];
571 }
572
573 public function archiveStage($board_id, $stage_id)
574 {
575 $board_id = absint($board_id);
576 $stage_id = absint($stage_id);
577
578 try {
579 $stage = $this->findStageOnBoard($stage_id, $board_id);
580
581 $updatedStage = $this->boardService->archiveStage($board_id, $stage);
582
583 return $this->sendSuccess([
584 'updatedStage' => $updatedStage,
585 'message' => __('Stage has been archived', 'fluent-boards'),
586 ], 200);
587 } catch (\Exception $e) {
588 return $this->sendError($e->getMessage(), 400);
589 }
590 }
591
592 public function restoreStage($board_id, $stage_id)
593 {
594 $board_id = absint($board_id);
595 $stage_id = absint($stage_id);
596
597 try {
598 $stage = $this->findStageOnBoard($stage_id, $board_id);
599
600 $updatedStage = $this->boardService->restoreStage($board_id, $stage);
601
602 return $this->sendSuccess([
603 'success' => true,
604 'updatedStage' => $updatedStage,
605 'message' => __('Stage has been restored', 'fluent-boards')
606 ], 200);
607 } catch (\Exception $e) {
608 return $this->sendError($e->getMessage(), 400);
609 }
610 }
611
612
613 public function repositionStages(Request $request, $board_id)
614 {
615 $incomingList = $request->get('list');
616 if (!is_array($incomingList)) {
617 $incomingList = [];
618 }
619 $incomingList = array_map('intval', $incomingList);
620 try {
621 foreach ($incomingList as $stageId) {
622 $this->findStageOnBoard($stageId, $board_id);
623 }
624
625 $this->boardService->repositionStages($board_id, $incomingList);
626 return $this->sendSuccess([
627 'message' => __('Stages Reordered', 'fluent-boards'),
628 'updatedStages' => $this->stageService->getLastOneMinuteUpdatedStages($board_id)
629 ], 200);
630 } catch (\Exception $e) {
631 return $this->sendError($e->getMessage(), 400);
632 }
633 }
634
635 public function getAssigneesByBoard($board_id)
636 {
637 return $this->sendSuccess([
638 'data' => $this->boardService->getAssigneesByBoard($board_id),
639 ], 200);
640 }
641
642 public function delete($board_id)
643 {
644 try {
645 if (!PermissionManager::isAdmin()) {
646 throw new \Exception(esc_html__('You do not have permission to delete this board', 'fluent-boards'), 400);
647 }
648 $this->boardService->deleteBoard($board_id);
649
650 return $this->sendSuccess([
651 'message' => __('Board has been deleted', 'fluent-boards'),
652 ], 200);
653 } catch (\Exception $e) {
654 return $this->sendError($e->getMessage(), 400);
655 }
656 }
657
658 public function getCurrencies()
659 {
660 return BoardHandler::getCurrencies();
661 }
662
663 public function getActivities(Request $request, $board_id)
664 {
665 try {
666 $activities = $this->boardService->getActivities($board_id, [
667 'per_page' => $request->getSafe('per_page', 'intval', 40),
668 'page' => $request->getSafe('page', 'intval', 1),
669 ]);
670 return $this->sendSuccess([
671 'activities' => $activities,
672 ], 200);
673 } catch (\Exception $e) {
674 return $this->sendError($e->getMessage(), 404);
675 }
676 }
677
678 /*
679 * TODO: Refactor this method - for Masiur
680 */
681 public function getBoardUsers($board_id)
682 {
683 $board = Board::findOrFail($board_id);
684
685 $boardObjects = Relation::where('object_type', 'board_user')
686 ->where('object_id', $board_id)
687 ->get()->keyBy('foreign_id');
688
689 $superAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
690 ->get()->pluck('object_id')->toArray();
691
692 $userIds = $boardObjects->pluck('foreign_id')->toArray();
693
694 $coreUsers = [];
695 if ($userIds) {
696 // Get the users who are in the board (members and managers
697 $coreUsers = get_users([
698 'include' => $userIds
699 ]);
700 }
701
702 $formattedUsers = [];
703
704 foreach ($coreUsers as $user) {
705 $name = trim($user->first_name . ' ' . $user->last_name);
706 if (!$name) {
707 $name = $user->display_name;
708 }
709
710 $boardRelation = $boardObjects[$user->ID] ?? null;
711
712
713 $formattedUsers[] = [
714 'ID' => $user->ID,
715 'display_name' => $name,
716 'user_login' => $user->user_login,
717 'email' => $user->user_email,
718 'photo' => fluent_boards_user_avatar($user->user_email, $name),
719 'role' => $this->boardUserRole($boardRelation),
720 'is_super' => in_array($user->ID, $superAdminIds),
721 'is_wpadmin' => $user->has_cap('manage_options')
722 ];
723 }
724
725 // order formatted users by display_name
726 usort($formattedUsers, function ($a, $b) {
727 return strcmp($a['display_name'], $b['display_name']);
728 });
729
730 $returnData = [
731 'users' => Helper::sanitizeUsersArray($formattedUsers, $board_id),
732 'global_admins' => []
733 ];
734
735 if (!PermissionManager::isAdmin(get_current_user_id())) {
736 return $returnData;
737 }
738
739 /*
740 * These are the rest of the Fluent Boards and WordPress admins who are not in the board.
741 */
742 $fluentBoardAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
743 ->whereNotIn('object_id', $userIds)
744 ->get()
745 ->pluck('object_id')
746 ->toArray();
747
748 $wordPressAdminIds = get_users([
749 'capability' => 'manage_options',
750 'exclude' => $userIds,
751 'fields' => 'ID',
752 ]);
753
754 $adminUserIds = array_values(array_unique(array_map('intval', array_merge($fluentBoardAdminIds, $wordPressAdminIds))));
755
756 if ($adminUserIds) {
757 $adminUsers = get_users([
758 'include' => $adminUserIds,
759 ]);
760
761 $formattedAdminUsers = [];
762
763 foreach ($adminUsers as $user) {
764 $name = trim($user->first_name . ' ' . $user->last_name);
765 if (!$name) {
766 $name = $user->display_name;
767 }
768
769 $formattedAdminUsers[] = [
770 'ID' => $user->ID,
771 'display_name' => $name,
772 'email' => $user->user_email,
773 'photo' => fluent_boards_user_avatar($user->user_email, $name),
774 'role' => 'admin',
775 'is_super' => in_array($user->ID, $superAdminIds),
776 'is_wpadmin' => $user->has_cap('manage_options')
777 ];
778 }
779
780 // order formatted users by display_name
781 usort($formattedAdminUsers, function ($a, $b) {
782 return strcmp($a['display_name'], $b['display_name']);
783 });
784
785 $returnData['global_admins'] = Helper::sanitizeUsersArray($formattedAdminUsers, $board_id);
786 }
787
788 return $this->sendSuccess($returnData, 200);
789 }
790
791
792 public function removeUserFromBoard($board_id, $userId)
793 {
794 $this->boardService->removeUserFromBoard($board_id, $userId);
795
796 if (!PermissionManager::isAdmin($userId)) {
797 $this->boardService->removeFromRecentlyOpened($board_id, $userId);
798 }
799
800 return [
801 'message' => __('Member removed successfully', 'fluent-boards'),
802 ];
803 }
804
805 public function addMembersInBoard(Request $request, $board_id)
806 {
807 $memberId = $request->getSafe('memberId');
808 $isViewerOnly = $request->getSafe('isViewerOnly');
809 $member = $this->boardService->addMembersInBoard($board_id, $memberId, $isViewerOnly);
810 if (!$member) {
811 return $this->sendError([
812 'message' => __('User already a member', 'fluent-boards'),
813 ], 304);
814 }
815
816
817 return [
818 'message' => __('Member added successfully', 'fluent-boards'),
819 'member' => Helper::sanitizeUserCollections($member)
820 ];
821 }
822
823 private function boardSanitizeAndValidate($data, array $rules = [])
824 {
825 $data = Helper::sanitizeBoard($data);
826
827 return $this->validate($data, $rules);
828 }
829
830 private function stageSanitizeAndValidate($data, array $rules = [])
831 {
832 $data = Helper::sanitizeStage($data);
833
834 return $this->validate($data, $rules);
835 }
836
837 private function taskSanitizeAndValidate($data, array $rules = [])
838 {
839 $data = Helper::sanitizeTask($data);
840
841 return $this->validate($data, $rules);
842 }
843
844 public function searchBoards(Request $request)
845 {
846 $per_page = $request->getSafe('per_page', 'intval', 10);
847 $search_input = $request->getSafe('searchInput', 'sanitize_text_field', '');
848 $type = $request->getSafe('type', 'sanitize_text_field', 'to-do');
849
850 $currentUserId = get_current_user_id();
851
852 if (PermissionManager::isAdmin($currentUserId)) {
853 $boards = Board::query()->where('type', $type)
854 ->where('title', 'like', '%' . $search_input . '%')
855 ->with('stages', 'tasks', 'users')
856 ->paginate($per_page);
857
858 foreach ($boards as $board) {
859 $board->users = Helper::sanitizeUserCollections($board->users);
860 }
861
862 } else {
863 $currentUser = User::find($currentUserId);
864 $boards = $currentUser->boards()->where('type', $type)->where('title', 'like', '%' . $search_input . '%')->paginate($per_page);
865 }
866
867 return [
868 'boards' => $boards,
869 ];
870 }
871
872 public function getUsersOfBoards()
873 {
874 $userBoards = $this->boardService->getUsersOfBoards();
875
876 return $this->sendSuccess([
877 'userBoards' => $userBoards,
878 ], 200);
879 }
880
881
882
883 /**
884 * Refactor this code form me - Masiur
885 * change stage settings is_public for roadmap user and admin view
886 * @param $board_id
887 * @param $stage_id
888 * @return
889 */
890 public function changeStageView($board_id, $stage_id)
891 {
892 $board_id = absint($board_id);
893 $stage_id = absint($stage_id);
894
895 try {
896 $stage = $this->findStageOnBoard($stage_id, $board_id);
897 $message = __('The stage is made public!', 'fluent-boards');
898 $settings = $stage->settings;
899
900 if (isset($settings['is_public'])) {
901 if ($settings['is_public']) {
902 $settings['is_public'] = false;
903 $message = __('The stage is made private!', 'fluent-boards');
904 } else {
905 $settings['is_public'] = true;
906 }
907 } else {
908 $settings['is_public'] = true;
909 }
910
911 $stage->settings = $settings;
912 $stage->save();
913 return $this->sendSuccess([
914 'message' => $message,
915 'stage' => $stage
916 ]);
917 } catch (\Exception $e) {
918 return $this->sendError($e->getMessage(), 400);
919 }
920 }
921
922
923 /**
924 * Set or reset board background image/color.
925 * @param \FluentBoards\Framework\Http\Request\Request $request
926 * @return
927 */
928 public function setBoardBackground(Request $request, $board_id)
929 {
930 $backgroundData = [];
931 $isResetRequest = $request->getSafe('reset', 'rest_sanitize_boolean');
932
933 if ($isResetRequest) {
934 $backgroundData = [
935 'reset' => true,
936 ];
937 } elseif ($request->image_url) {
938 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
939 "id" => 'required',
940 'image_url' => 'required|string|url',
941 ]);
942 } elseif ($request->color) {
943 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
944 "id" => 'required',
945 'color' => 'required',
946 ]);
947 }
948
949 try {
950 if (!$board_id) {
951 $errorMessage = __('Board id is required', 'fluent-boards');
952 throw new \Exception(esc_html($errorMessage), 400);
953 }
954
955 if (empty($backgroundData)) {
956 $errorMessage = __('Background data is required', 'fluent-boards');
957 throw new \Exception(esc_html($errorMessage), 400);
958 }
959
960 return $this->sendSuccess([
961 'message' => __('Background updated successfully', 'fluent-boards'),
962 'background' => $this->boardService->setBoardBackground($backgroundData, $board_id),
963 ]);
964 } catch (\Exception $e) {
965 return $this->sendError($e->getMessage(), 400);
966 }
967 }
968
969
970 /**
971 * Summary of getStageTaskAvailablePositions
972 * @param mixed $board_id
973 * @param mixed $stage_slug
974 * @return $availablePositions as an array
975 * @throws \Exception
976 */
977 public function getStageTaskAvailablePositions(Request $request, $board_id, $stage_id)
978 {
979 try {
980 if ($board_id && $stage_id) {
981 $taskId = $request->getSafe('task_id', 'intval');
982 $availablePositions = $this->boardService->getStageTaskAvailablePositions($board_id, $stage_id, $taskId);
983 return $this->sendSuccess([
984 'availablePositions' => $availablePositions['availablePositions'],
985 'moveTargets' => $availablePositions['moveTargets'],
986 'currentMoveTargetKey' => $availablePositions['currentMoveTargetKey'],
987 'defaultMoveTargetKey' => $availablePositions['defaultMoveTargetKey'],
988 ], 200);
989 } else {
990 $message = '';
991 if (!$board_id) {
992 $message = 'Board id ';
993 }
994 if (!$stage_id) {
995 $message = 'Stage ';
996 }
997 throw new \Exception(esc_html($message . 'is required'), 400);
998 }
999 } catch (\Exception $e) {
1000 return $this->sendError($e->getMessage(), 400);
1001 }
1002 }
1003
1004 public function getAssociateCrmContacts($board_id)
1005 {
1006 try {
1007 $contactAssociatedTasks = Task::with('board')->where('board_id', $board_id)
1008 ->whereNotNull('crm_contact_id')
1009 ->get();
1010
1011 $tasksByContact = [];
1012 foreach ($contactAssociatedTasks as $task) {
1013 $tasksByContact[absint($task->crm_contact_id)][] = $task;
1014 }
1015
1016 $boardContactIds = Meta::query()
1017 ->where('object_id', absint($board_id))
1018 ->where('object_type', Constant::OBJECT_TYPE_BOARD)
1019 ->whereIn('key', [
1020 Constant::BOARD_ASSOCIATED_CRM_CONTACT,
1021 self::LEGACY_BOARD_ASSOCIATED_CRM_CONTACT,
1022 ])
1023 ->pluck('value')
1024 ->toArray();
1025 $boardContactIds = array_values(array_unique(array_filter(array_map('absint', $boardContactIds))));
1026
1027 $contactIds = array_values(array_unique(array_filter(array_map('absint', array_merge(
1028 array_keys($tasksByContact),
1029 $boardContactIds
1030 )))));
1031
1032 usort($contactIds, function ($firstContactId, $secondContactId) use ($boardContactIds) {
1033 return (int) in_array($secondContactId, $boardContactIds, true) - (int) in_array($firstContactId, $boardContactIds, true);
1034 });
1035
1036 $formattedContacts = Collection::make($contactIds)
1037 ->map(function ($contactId) use ($tasksByContact, $boardContactIds) {
1038 $contact = Helper::crm_contact($contactId);
1039 if (!$contact) {
1040 return null; // Skip if subscriber not found
1041 }
1042
1043 $tasks = $tasksByContact[$contactId] ?? [];
1044 $contact['name'] = trim(($contact['first_name'] ?? '') . ' ' . ($contact['last_name'] ?? '')) ?: ($contact['full_name'] ?? $contact['email'] ?? '');
1045 $contact['crm_contact_id'] = $contactId;
1046 $contact['is_board_contact'] = in_array($contactId, $boardContactIds, true);
1047 $contact['tasks'] = $tasks;
1048
1049 return $contact;
1050 })
1051 ->filter()->toArray();
1052
1053
1054 return $this->sendSuccess([
1055 'associatedContacts' => $formattedContacts
1056 ], 200);
1057 } catch (\Exception $e) {
1058 return $this->sendError($e->getMessage(), 404);
1059 }
1060 }
1061
1062 public function updateAssociateCrmContact(Request $request, $board_id)
1063 {
1064 $value = $request->getSafe('value');
1065 $this->boardService->updateAssociateMember($value, $board_id);
1066
1067 return $this->sendSuccess([
1068 'message' => __('Associated Crm Member has been updated', 'fluent-boards'),
1069 ], 200);
1070 }
1071
1072 public function hasDataChanged(Request $request, $board_id)
1073 {
1074 $includeArchived = filter_var($request->get('include_archived', false), FILTER_VALIDATE_BOOLEAN);
1075 $since = $request->getSafe('since', 'sanitize_text_field');
1076 return $this->boardService->hasDataChanged($board_id, $includeArchived, $since);
1077 }
1078
1079 public function createStage(Request $request, $board_id)
1080 {
1081 $stageData = $this->stageSanitizeAndValidate($request->all(), [
1082 'title' => 'required|string',
1083 'position' => 'nullable|numeric'
1084 ]);
1085
1086 $board = Board::find($board_id);
1087 $stage = $this->stageService->createStage($stageData, $board_id);
1088
1089 do_action('fluent_boards/board_stage_added', $board, $stage);
1090
1091 $updatedStates = (new StageService())->getLastOneMinuteUpdatedStages($board_id);
1092
1093 return [
1094 'updatedStages' => $updatedStates,
1095 'message' => __('stage has been created', 'fluent-boards'),
1096 ];
1097 }
1098
1099 public function moveAllTasks(Request $request, $board_id)
1100 {
1101 $oldStageId = $request->getSafe('oldStageId', 'intval');
1102 $newStageId = $request->getSafe('newStageId', 'intval');
1103
1104 if (!$oldStageId || !$newStageId) {
1105 return $this->sendError(__('Invalid stage IDs provided', 'fluent-boards'), 400);
1106 }
1107
1108 // Verify stages exist and belong to the board
1109 $oldStage = Stage::where('id', $oldStageId)->where('board_id', $board_id)->first();
1110 $newStage = Stage::where('id', $newStageId)->where('board_id', $board_id)->first();
1111
1112 if (!$oldStage || !$newStage) {
1113 return $this->sendError(__('One or both stages do not exist or do not belong to this board', 'fluent-boards'), 400);
1114 }
1115
1116 $updates = $this->stageService->moveAllTasks($oldStageId, $newStageId, $board_id);
1117
1118 return [
1119 'message' => __('Tasks have been moved', 'fluent-boards'),
1120 'updatedTasks' => $updates,
1121 ];
1122
1123 }
1124
1125 public function archiveAllTasksInStage($board_id, $stage_id)
1126 {
1127 $board_id = absint($board_id);
1128 $stage_id = absint($stage_id);
1129
1130 try {
1131 $this->findStageOnBoard($stage_id, $board_id);
1132 $updates = $this->stageService->archiveAllTasksInStage($stage_id, $board_id);
1133
1134 return [
1135 'message' => __('Tasks have been archived', 'fluent-boards'),
1136 'updatedTasks' => $updates,
1137 ];
1138 } catch (\Exception $e) {
1139 return $this->sendError($e->getMessage(), 400);
1140 }
1141 }
1142
1143 public function getAssociatedBoards(Request $request, $associated_id)
1144 {
1145 if (!$this->currentUserCanReadCrmContacts()) {
1146 return $this->sendError(esc_html__('You do not have permission to view CRM contact boards', 'fluent-boards'), 403);
1147 }
1148
1149 $associatedId = absint($associated_id);
1150
1151 if (!$associatedId) {
1152 return $this->sendError(__('Invalid CRM contact', 'fluent-boards'), 400);
1153 }
1154
1155 $associatedBoards = $this->boardService->getAssociatedBoards($associatedId, get_current_user_id());
1156
1157 return [
1158 'boards' => $associatedBoards,
1159 ];
1160 }
1161
1162 private function currentUserCanReadCrmContacts()
1163 {
1164 $permissionManager = 'FluentCrm\\App\\Services\\PermissionManager';
1165
1166 if (!class_exists($permissionManager)) {
1167 return false;
1168 }
1169
1170 return (bool) $permissionManager::currentUserCan('fcrm_read_contacts');
1171 }
1172
1173 public function duplicateBoard(Request $request, $board_id)
1174 {
1175 $boardData = $this->taskSanitizeAndValidate($request->get('board'), [
1176 'title' => 'required|string'
1177 ]);
1178
1179 $boardData['source_board_id'] = $board_id;
1180
1181 $isWithLabels = $request->getSafe('isWithLabels');
1182 $isWithTasks = $request->getSafe('isWithTasks');
1183 $isWithTemplates = $request->getSafe('isWithTemplates');
1184
1185 try {
1186 if(!PermissionManager::isAdmin()) {
1187 $errorMessage = __('You do not have permission to duplicate board', 'fluent-boards');
1188 throw new \Exception(esc_html($errorMessage), 400);
1189 }
1190 //create board
1191 $newBoard = $this->boardService->copyBoard($boardData);
1192
1193 //label copy
1194 $labelMap = [];
1195
1196 if ($isWithLabels == 'yes') {
1197 $labelMap = $this->labelService->copyLabelsOfBoard($board_id, $newBoard);
1198 }
1199
1200 //stage copy
1201 $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id, $isWithTemplates);
1202
1203 //copy tasks of selected stages
1204 if ($isWithTasks == 'yes') {
1205 $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap,$isWithTemplates);
1206 }
1207
1208 return $this->sendSuccess([
1209 'board' => $newBoard,
1210 ], 200);
1211 } catch (\Exception $e) {
1212 return $this->sendError($e->getMessage(), 400);
1213 }
1214 }
1215
1216 public function importFromBoard(Request $request, $board_id)
1217 {
1218 $selectedStages = $request->getSafe('selectedStages');
1219 $position = $request->getSafe('position', 'intval');
1220
1221 // Validate and sanitize selectedStages array
1222 if (!is_array($selectedStages)) {
1223 $selectedStages = [$selectedStages];
1224 }
1225 $selectedStages = array_filter(array_map('intval', $selectedStages));
1226
1227 try {
1228 $this->stageService->importStagesFromBoard($board_id, $selectedStages, $position);
1229
1230 return $this->sendSuccess([
1231 'message' => __('Import successfully', 'fluent-boards'),
1232 ], 200);
1233
1234 } catch (\Exception $e) {
1235 return $this->sendError($e->getMessage(), 400);
1236 }
1237 }
1238
1239 public function getBoardDefaultBackgroundColors()
1240 {
1241 return [
1242 'solidColors' => Constant::BOARD_BACKGROUND_DEFAULT_SOLID_COLORS,
1243 'gradients' => Constant::BOARD_BACKGROUND_DEFAULT_GRADIENT_COLORS
1244 ];
1245 }
1246
1247 /*
1248 * TODO: For Masiur - I will update this later
1249 */
1250 public function updateBoardProperties(Request $request, $board_id)
1251 {
1252 $pageId = $request->getSafe('page_id');
1253 $enable_stage_change_email = $request->getSafe('enable_stage_change_email');
1254
1255 $board = Board::findOrFail($board_id);
1256
1257 $board->updateMeta('roadmap_page_id', $pageId);
1258 $board->updateMeta('enable_stage_change_email', $enable_stage_change_email);
1259
1260 $board = $board->fresh();
1261
1262 return [
1263 'message' => __('Board has been updated', 'fluent-boards'),
1264 'board' => apply_filters('fluent_boards/board_find', $board)
1265 ];
1266 }
1267
1268 public function archiveBoard($board_id)
1269 {
1270 try {
1271 $board = $this->boardService->archiveBoard($board_id);
1272
1273 return [
1274 'board' => $board,
1275 'message' => __('Board has been archived successfully!', 'fluent-boards')
1276 ];
1277 } catch (\Exception $e) {
1278 return $this->sendError($e->getMessage(), 400);
1279 }
1280 }
1281
1282 public function restoreBoard($board_id)
1283 {
1284 try {
1285 $board = $this->boardService->restoreBoard($board_id);
1286
1287 return [
1288 'board' => $board,
1289 'message' => __('Board has been restored successfully!', 'fluent-boards')
1290 ];
1291 } catch (\Exception $e) {
1292 return $this->sendError($e->getMessage(), 400);
1293 }
1294 }
1295
1296 private function boardUserRole($boardRelation)
1297 {
1298 return $boardRelation && Arr::get($boardRelation->settings, 'is_admin')
1299 ? 'manager'
1300 : ($boardRelation && Arr::has($boardRelation->settings, 'is_viewer_only') && Arr::get($boardRelation->settings, 'is_viewer_only')
1301 ? 'viewer'
1302 : 'member');
1303 }
1304 public function uploadBoardBackground(Request $request,$board_id)
1305 {
1306 $file = Arr::get($request->files(), 'file')->toArray();
1307 (new \FluentBoards\App\Services\UploadService)->validateFile($file);
1308
1309 $uploadInfo = UploadService::handleFileUpload( $request->files(), $board_id);
1310
1311 $fileData = $uploadInfo[0];
1312 $initialDataData = [
1313 'type' => 'url',
1314 'url' => '',
1315 'name' => '',
1316 'size' => 0,
1317 ];
1318
1319 $attachData = array_merge($initialDataData, $fileData);
1320 $UrlMeta = [];
1321 if($attachData['type'] == 'url') {
1322 $UrlMeta = RemoteUrlParser::parse($attachData['url']);
1323 }
1324 $uid = wp_generate_uuid4();
1325 $fileUploadedData = new Attachment();
1326 $fileUploadedData->object_id = $board_id;
1327 $fileUploadedData->object_type = Constant::BOARD_BACKGROUND_IMAGE;
1328 $fileUploadedData->attachment_type = $attachData['type'];
1329 $fileUploadedData->title = (new TaskService())->setTitle($attachData['type'], $attachData['name'], $UrlMeta);
1330 $fileUploadedData->file_path = $attachData['type'] != 'url' ? $attachData['file'] : null;
1331 $fileUploadedData->full_url = esc_url($attachData['url']);
1332 $fileUploadedData->file_size = $attachData['size'];
1333 $fileUploadedData->settings = $attachData['type'] == 'url' ? [
1334 'meta' => $UrlMeta
1335 ] : '';
1336 $fileUploadedData->driver = 'local';
1337 $fileUploadedData->file_hash = md5($uid . wp_rand(0, 1000));
1338 $fileUploadedData->save();
1339 if(!!defined('FLUENT_BOARDS_PRO_VERSION')) {
1340 $mediaData = (new AttachmentService())->processMediaData($fileData, $file);
1341 $fileUploadedData['driver'] = $mediaData['driver'];
1342 $fileUploadedData['file_path'] = $mediaData['file_path'];
1343 $fileUploadedData['full_url'] = $mediaData['full_url'];
1344 $fileUploadedData->save();
1345 }
1346
1347 $board = Board::find($board_id);
1348 $oldBackground = $board->background;
1349 $publicUrl = (new CommentService())->createPublicUrl($fileUploadedData, $board_id);
1350 $background = [
1351 'color' => null,
1352 'id' => $fileUploadedData->id,
1353 'image_url' => $publicUrl,
1354 'is_image' => true,
1355 ];
1356 $board->background = $background;
1357 $board->save();
1358 do_action('fluent_boards/board_background_updated', $board_id, $oldBackground);
1359
1360 return $this->sendSuccess([
1361 'message' => __('Background updated successfully', 'fluent-boards'),
1362 'background' => $board->background,
1363 ]);
1364 }
1365
1366 public function getPinnedBoards()
1367 {
1368 $pinnedBoards = $this->boardService->getPinnedBoards();
1369
1370 return $this->sendSuccess([
1371 'pinnedBoards' => $pinnedBoards,
1372 ], 200);
1373 }
1374
1375 public function pinBoard($boardId)
1376 {
1377 $this->boardService->pinBoard($boardId);
1378
1379 return $this->sendSuccess([
1380 'message' => __('The Board has been pinned', 'fluent-boards'),
1381 ], 200);
1382 }
1383
1384 public function unpinBoard($boardId)
1385 {
1386 $remove = $this->boardService->unpinBoard($boardId);
1387
1388 if (!$remove) {
1389 return $this->sendError([
1390 'message' => __('Board is not pinned', 'fluent-boards'),
1391 ], 400);
1392 }
1393
1394 return $this->sendSuccess([
1395 'message' => __('Board is removed from pinned boards', 'fluent-boards'),
1396 ], 200);
1397 }
1398
1399 public function getBoardFolder($board_id)
1400 {
1401 try {
1402 $folder = $this->boardService->getBoardFolder($board_id);
1403 return $this->sendSuccess([
1404 'folder' => $folder,
1405 ], 200);
1406 } catch (\Exception $e) {
1407 return $this->sendError($e->getMessage(), 400);
1408 }
1409 }
1410
1411 public function getBoardMenuItems($board_id)
1412 {
1413 try {
1414 $menuItems = (new BoardMenuHandler())->getMenuItems($board_id);
1415
1416 return $this->sendSuccess([
1417 'menu_items' => $menuItems
1418 ], 200);
1419 } catch (\Exception $e) {
1420 return $this->sendError($e->getMessage(), 500);
1421 }
1422 }
1423
1424 public function getPublicAccessSettings($board_id)
1425 {
1426 $board_id = absint($board_id);
1427 $board = Board::findOrFail($board_id);
1428
1429 $enabled = (bool) $board->getMetaByKey('public_access_enabled');
1430 $shortcode = $enabled ? '[fluent_board_public id="' . $board_id . '"]' : '';
1431
1432 return $this->sendSuccess([
1433 'enabled' => $enabled,
1434 'shortcode' => $shortcode,
1435 ], 200);
1436 }
1437
1438 public function togglePublicAccess(Request $request, $board_id)
1439 {
1440 $board_id = absint($board_id);
1441 $board = Board::findOrFail($board_id);
1442
1443 $enabled = filter_var(
1444 $request->getSafe('enabled', 'sanitize_text_field', false),
1445 FILTER_VALIDATE_BOOLEAN
1446 );
1447
1448 $board->updateMeta('public_access_enabled', $enabled ? '1' : '');
1449
1450 $shortcode = $enabled ? '[fluent_board_public id="' . $board_id . '"]' : '';
1451
1452 return $this->sendSuccess([
1453 'message' => $enabled
1454 ? __('Public access has been enabled', 'fluent-boards')
1455 : __('Public access has been disabled', 'fluent-boards'),
1456 'enabled' => $enabled,
1457 'shortcode' => $shortcode,
1458 ], 200);
1459 }
1460
1461 /**
1462 * Resolve a stage only when it belongs to the requested board.
1463 *
1464 * @param int $stageId
1465 * @param int $boardId
1466 * @return Stage
1467 * @throws \Exception
1468 */
1469 private function findStageOnBoard($stageId, $boardId)
1470 {
1471 $stage = Stage::where('id', absint($stageId))
1472 ->where('board_id', absint($boardId))
1473 ->first();
1474
1475 if (!$stage) {
1476 throw new \Exception(esc_html__('Stage not found', 'fluent-boards'));
1477 }
1478
1479 return $stage;
1480 }
1481
1482 }
1483