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 / BoardController.php

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

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