PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / trunk
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration vtrunk
2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 1.40 1.41 1.45 All 41 releases
← All changes | app/Http/Controllers/BoardController.php +662 -134 1.41trunk View file →
@@ -1,14 +1,17 @@
1 1 <?php
2 2
3 3 namespace FluentBoards\App\Http\Controllers;
4 4
5 +use FluentBoards\App\Models\Attachment;
5 6 use FluentBoards\App\Models\Meta;
6 7 use FluentBoards\App\Models\Relation;
7 8 use FluentBoards\App\Models\Task;
8 9 use FluentBoards\App\Models\User;
9 10 use FluentBoards\App\Models\Board;
11 +use FluentBoards\App\Services\CommentService;
10 12 use FluentBoards\App\Services\Constant;
13 +use FluentBoards\App\Services\DescriptionMarkdownConverter;
11 14 use FluentBoards\App\Services\Helper;
12 15 use FluentBoards\App\Models\Stage;
13 16 use FluentBoards\App\Services\InstallService;
14 17 use FluentBoards\App\Services\StageService;
@@ -13,19 +16,26 @@
13 16 use FluentBoards\App\Services\InstallService;
14 17 use FluentBoards\App\Services\StageService;
15 18 use FluentBoards\App\Services\TaskService;
16 19 use FluentBoards\App\Services\BoardService;
17 -use FluentBoards\App\Services\UserService;
20 +use FluentBoards\App\Services\FolderService;
21 +use FluentBoards\App\Services\UploadService;
18 22 use FluentBoards\Framework\Http\Request\Request;
19 23 use FluentBoards\App\Services\PermissionManager;
24 +use FluentBoards\App\Services\PublicAccessService;
20 25 use FluentBoards\App\Hooks\Handlers\BoardHandler;
26 +use FluentBoards\App\Hooks\Handlers\BoardMenuHandler;
21 27 use FluentBoards\App\Services\LabelService;
22 28 use FluentBoards\Framework\Support\Arr;
23 29 use FluentBoards\Framework\Support\Collection;
24 -use FluentCrm\App\Models\Subscriber;
30 +use FluentBoardsPro\App\Services\AttachmentService;
31 +use FluentBoardsPro\App\Services\CustomFieldService;
32 +use FluentBoardsPro\App\Services\RemoteUrlParser;
25 33
26 34 class BoardController extends Controller
27 35 {
36 + private const LEGACY_BOARD_ASSOCIATED_CRM_CONTACT = 'crm_contact';
37 +
28 38 private $boardService;
29 39 private $taskService;
30 40 private $stageService;
31 41 private $labelService;
@@ -45,9 +55,10 @@
45 55 }
46 56
47 57 public function getBoards(Request $request)
48 58 {
49 - $per_page = $request->getSafe('per_page', 'intval', 20);
59 + $per_page = $request->getSafe('per_page', 'intval', 100);
60 + $per_page = max(1, min(100, $per_page));
50 61 $userId = get_current_user_id();
51 62 $type = $request->getSafe('type', 'sanitize_text_field', 'to-do');
52 63
53 64 $order = $request->getSafe('order', 'sanitize_text_field', 'created_at');
@@ -54,18 +65,19 @@
54 65 $orderBy = $request->getSafe('orderBy', 'sanitize_text_field', 'DESC');
55 66 $searchInput = $request->getSafe('searchInput', 'sanitize_text_field');
56 67
57 68 $option = $request->getSafe('option', 'sanitize_text_field');
69 + $folderId = $request->getSafe('fid', 'intval'); // Get folder ID from request
58 70
59 - if(!defined('FLUENT_ROADMAP'))
60 - {
61 - if($option == 'archived') {
71 + // Initialize the query based on archive status
72 + if (!defined('FLUENT_ROADMAP')) {
73 + if ($option == 'archived') {
62 74 $relatedBoardsQuery = Board::whereNotNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
63 75 } else {
64 76 $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
65 77 }
66 78 } else {
67 - if($option == 'archived') {
79 + if ($option == 'archived') {
68 80 $relatedBoardsQuery = Board::whereNotNull('archived_at')->byAccessUser($userId);
69 81 } else {
70 82 $relatedBoardsQuery = Board::whereNull('archived_at')->byAccessUser($userId);
71 83 }
@@ -70,8 +82,30 @@
70 82 $relatedBoardsQuery = Board::whereNull('archived_at')->byAccessUser($userId);
71 83 }
72 84 }
73 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
74 108 if (!empty($searchInput)) {
75 109 $relatedBoardsQuery = $relatedBoardsQuery->where('title', 'like', '%' . $searchInput . '%');
76 110 }
77 111
@@ -80,17 +114,59 @@
80 114 ->with('stages', 'users')
81 115 ->paginate($per_page);
82 116
83 117 foreach ($relatedBoards as $relatedBoard) {
118 + $relatedBoard->description = DescriptionMarkdownConverter::normalize($relatedBoard->description);
84 119 $relatedBoard->users = Helper::sanitizeUserCollections($relatedBoard->users);
120 + $relatedBoard->is_pinned = $this->boardService->isPinned($relatedBoard->id);
85 121 }
86 122
87 - return $this->sendSuccess([
88 - 'boards' => $relatedBoards
89 - ], 200);
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);
90 137 }
91 138
92 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 + /**
93 169 * Get the list of boards and their associated stages for the current user.
94 170 *
95 171 * @param \FluentBoards\Framework\Http\Request\Request $request
96 172 * @return \WP_REST_Response
@@ -96,30 +172,56 @@
96 172 * @return \WP_REST_Response
97 173 */
98 174 public function getBoardsList(Request $request)
99 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 + {
100 198 try {
101 199 $userId = get_current_user_id();
102 200
103 - // Query to fetch boards that are not archived and accessible by the user
104 - // Check if the FLUENT_ROADMAP constant is defined
105 - if (!defined('FLUENT_ROADMAP')) {
106 - $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
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);
107 207 } else {
108 - $relatedBoardsQuery = Board::whereNull('archived_at')->byAccessUser($userId);
208 + $relatedBoardsQuery = Board::whereNull('archived_at')->excludeTemplates()->byAccessUser($userId);
109 209 }
110 210
111 - $relatedBoards = $relatedBoardsQuery->with('stages')->get();
211 + if (!empty($searchInput)) {
212 + $relatedBoardsQuery = $relatedBoardsQuery->where('title', 'like', '%' . $searchInput . '%');
213 + }
112 214
113 - // Fetch the stages associated with the boards
114 - $stages = Stage::whereIn('board_id', $relatedBoards->pluck('id'))->where('archived_at', null)->get();
215 + $relatedBoards = $relatedBoardsQuery->orderBy('created_at', 'DESC')->get();
115 216
116 217 return $this->sendSuccess([
117 - 'boards' => $relatedBoards,
118 - 'all_stages' => $stages,
119 - ], 200);
218 + 'boards' => $relatedBoards
219 + ]);
120 220 } catch (\Exception $e) {
121 - return $this->sendError($e->getMessage(), 404);
221 + return $this->sendError([
222 + 'message' => $e->getMessage()
223 + ]);
122 224 }
123 225 }
124 226
125 227 public function getRecentBoards()
@@ -126,9 +228,12 @@
126 228 {
127 229 $boards = $this->boardService->getRecentBoards();
128 230
129 231 if (!$boards || $boards->isEmpty()) {
130 - $boards = Board::where('type', 'to-do')->byAccessUser(get_current_user_id())
232 + $boards = Board::whereNull('archived_at')
233 + ->excludeTemplates()
234 + ->availableInCurrentInstall()
235 + ->byAccessUser(get_current_user_id())
131 236 ->limit(4)
132 237 ->withCount('completedTasks')
133 238 ->with(['stages', 'users'])
134 239 ->get();
@@ -135,8 +240,9 @@
135 240 }
136 241
137 242 foreach ($boards as $board) {
138 243 $board->users = Helper::sanitizeUserCollections($board->users);
244 + $board->is_pinned = $this->boardService->isPinned($board->id);
139 245 }
140 246
141 247 return [
142 248 'boards' => $boards,
@@ -151,9 +257,9 @@
151 257 $boards = $this->boardService->getBoardsByType($type);
152 258
153 259 return $this->sendSuccess([
154 260 'boards' => $boards,
155 - ], 200);
261 + ]);
156 262 }
157 263
158 264 public function createFirstBoard(Request $request)
159 265 {
@@ -164,11 +270,14 @@
164 270 'currency' => 'nullable|string',
165 271 'crm_contact_id' => 'nullable|numeric',
166 272 ]);
167 273
168 - $installFluentCRM = $request->get('withFluentCRM') == 'yes' ? true : false;
274 + $installFluentCRM = $request->getSafe('withFluentCRM', 'sanitize_text_field') == 'yes' ? true : false;
169 275
170 276 $postStages = $request->get('stages');
277 + if (!is_array($postStages)) {
278 + $postStages = [];
279 + }
171 280 $stageData = array();
172 281 foreach ($postStages as $stage) {
173 282 $temp = $this->stageSanitizeAndValidate($stage, [
174 283 'title' => 'required|string',
@@ -194,9 +303,9 @@
194 303 $this->taskService->createTask($taskData, $board->id);
195 304 }
196 305
197 306 do_action('fluent_boards/board_created', $board);
198 -
307 +
199 308 if ($installFluentCRM && !defined('FLUENTCRM')) {
200 309 InstallService::install('fluent-crm');
201 310 }
202 311
@@ -226,17 +335,44 @@
226 335 'description' => 'nullable',
227 336 'type' => 'required|string',
228 337 'currency' => 'nullable|string',
229 338 'crm_contact_id' => 'nullable|numeric',
339 + 'folder_id' => 'nullable',
230 340 ]);
231 341
232 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 +
233 354 $board = $this->boardService->createBoard($boardData);
234 - $this->labelService->createDefaultLabel($board->id);
355 + $this->createBoardLabelsFromRequest($request, $board->id);
356 + $this->addBoardMembersFromRequest($request, $board->id);
235 357 $type = ucfirst($boardData['type']);
358 + $stages = $request->get('stages');
359 + $sanitizedStages = [];
236 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 +
237 371 if (isset($boardData['type']) && $boardData['type'] == 'roadmap') {
238 - $this->stageService->createRoadmapStages($board, $request->get('stages'));
372 + $this->stageService->createRoadmapStages($board, $sanitizedStages);
373 + } elseif (!empty($sanitizedStages)) {
374 + $this->stageService->createStages($board, $sanitizedStages);
239 375 } else {
240 376 $this->stageService->createDefaultStages($board);
241 377 }
242 378
@@ -246,37 +382,118 @@
246 382 }
247 383
248 384 do_action('fluent_boards/board_created', $board);
249 385
386 +
387 + if ($folderId) {
388 + $folderService->addBoardToFolder($folderId, [$board->id]);
389 + }
390 +
250 391 $message = __('Board has been created successfully', 'fluent-boards');
251 392
252 - return $this->sendSuccess([
393 + return $this->send([
253 394 'message' => $message,
254 395 'board' => $board,
255 396 ], 201);
256 397 } catch (\Exception $e) {
257 - return $this->sendError($e->getMessage(), 400);
398 + return $this->sendError([
399 + 'message' => $e->getMessage()
400 + ]);
258 401 }
259 402 }
260 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 + */
261 484 public function getArchivedStage(Request $request, $board_id)
262 485 {
263 486 try {
264 - $pagination = $request->noPagination ? true : false;
265 - $per_page = isset($data['per_page']) ? $data['per_page'] : 30;
266 - $page = isset($data['page']) ? $data['page'] : 1;
267 - if ($pagination) {
268 - $stages = Stage::where('board_id', $board_id)
269 - ->whereNotNull('archived_at')
270 - ->orderBy('created_at', 'DESC')
271 - ->get();
272 - } else {
273 - $stages = Stage::where('board_id', $board_id)
274 - ->whereNotNull('archived_at')
275 - ->orderBy('created_at', 'DESC')
276 - ->paginate($per_page, ['*'], 'page', $page);
277 - }
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 + ];
278 493
494 + $stages = $this->stageService->getArchivedStages($sanitizedParams, $board_id);
495 +
279 496 return $this->sendSuccess([
280 497 'stages' => $stages,
281 498 ], 200);
282 499 } catch (\Exception $e) {
@@ -283,17 +500,33 @@
283 500 return $this->sendError($e->getMessage(), 404);
284 501 }
285 502 }
286 503
287 - public function find($board_id)
504 + public function find(Request $request, $board_id)
288 505 {
289 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);
290 509 $board->background = maybe_unserialize($board->background);
291 510 $board->createdOn = $board->created_at->format('Y-m-d');
292 511
293 - $board->load(['users', 'stages', 'labels', 'owner']);
512 + $board->load(['users', 'labels', 'owner']);
294 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 +
295 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 +
296 529 $board->load(['customFields']);
297 530 }
298 531
299 532 $this->boardService->updateRecentBoards($board_id);
@@ -303,17 +536,30 @@
303 536
304 537 $board->users = Helper::sanitizeUserCollections($board->users);
305 538 $board->owner = Helper::sanitizeUserCollections($board->owner);
306 539
540 + $board->is_pinned = $this->boardService->isPinned($board->id);
541 +
307 542 $board = apply_filters('fluent_boards/board_find', $board);
308 543
309 544 return [
310 - 'board' => $board
545 + 'board' => $board,
546 + 'synced_at' => current_time('mysql')
311 547 ];
312 548 }
313 549
314 550 public function update(Request $request, $board_id)
315 551 {
552 + // Board identity (title/description) is manager-only. This action shares the
553 + // `update` name with CommentController@update under the same policy group, so the
554 + // guard lives here rather than in a SingleBoardPolicy::update() method that would
555 + // also block ordinary members from editing their own comments.
556 + if (!PermissionManager::isBoardManager(absint($board_id))) {
557 + return $this->sendError([
558 + 'message' => __('You do not have permission to edit this board.', 'fluent-boards'),
559 + ], 403);
560 + }
561 +
316 562 $boardData = $this->boardSanitizeAndValidate($request->only(['title', 'description']), [
317 563 'title' => 'required|string',
318 564 'description' => 'nullable|string',
319 565 ]);
@@ -318,8 +564,9 @@
318 564 'description' => 'nullable|string',
319 565 ]);
320 566
321 567 $board = Board::findOrFail($board_id);
568 + $boardData['description'] = DescriptionMarkdownConverter::normalize($boardData['description']);
322 569
323 570 $oldBoard = clone $board;
324 571 $board->fill($boardData);
325 572 $board->save();
@@ -326,21 +573,23 @@
326 573
327 574 do_action('fluent_boards/board_updated', $board, $oldBoard);
328 575
329 576 return [
330 - 'stages' => $board->stages()->get(),
331 577 'message' => __('Board has been updated', 'fluent-boards'),
332 578 'board' => $board,
579 + 'stages' => $board->stages()->get(),
333 580 ];
334 581 }
335 582
336 583 public function archiveStage($board_id, $stage_id)
337 584 {
585 + $board_id = absint($board_id);
586 + $stage_id = absint($stage_id);
587 +
338 588 try {
339 - $stage = Stage::findOrFail($stage_id);
340 - $board = Board::findOrFail($stage->board_id);
589 + $stage = $this->findStageOnBoard($stage_id, $board_id);
341 590
342 - $updatedStage = $this->boardService->archiveStage($board->id, $stage);
591 + $updatedStage = $this->boardService->archiveStage($board_id, $stage);
343 592
344 593 return $this->sendSuccess([
345 594 'updatedStage' => $updatedStage,
346 595 'message' => __('Stage has been archived', 'fluent-boards'),
@@ -351,18 +600,20 @@
351 600 }
352 601
353 602 public function restoreStage($board_id, $stage_id)
354 603 {
604 + $board_id = absint($board_id);
605 + $stage_id = absint($stage_id);
606 +
355 607 try {
356 - $stage = Stage::findOrFail($stage_id);
357 - $board = Board::findOrFail($board_id);
608 + $stage = $this->findStageOnBoard($stage_id, $board_id);
358 609
359 - $updatedStage = $this->boardService->restoreStage($board->id, $stage);
610 + $updatedStage = $this->boardService->restoreStage($board_id, $stage);
360 611
361 612 return $this->sendSuccess([
362 - 'success' => true,
613 + 'success' => true,
363 614 'updatedStage' => $updatedStage,
364 - 'message' => __('Stage has been restored', 'fluent-boards')
615 + 'message' => __('Stage has been restored', 'fluent-boards')
365 616 ], 200);
366 617 } catch (\Exception $e) {
367 618 return $this->sendError($e->getMessage(), 400);
368 619 }
@@ -368,32 +619,22 @@
368 619 }
369 620 }
370 621
371 622
372 - public function changePositionOfStage(Request $request, $board_id)
623 + public function repositionStages(Request $request, $board_id)
373 624 {
374 - $changeData = $this->boardSanitizeAndValidate($request->only(['fromPosition', 'toPosition']), [
375 - 'fromPosition' => 'required',
376 - 'toPosition' => 'required',
377 - ]);
378 -
625 + $incomingList = $request->get('list');
626 + if (!is_array($incomingList)) {
627 + $incomingList = [];
628 + }
629 + $incomingList = array_map('intval', $incomingList);
379 630 try {
380 - $this->boardService->changePositionOfStage($board_id, $changeData);
631 + foreach ($incomingList as $stageId) {
632 + $this->findStageOnBoard($stageId, $board_id);
633 + }
381 634
635 + $this->boardService->repositionStages($board_id, $incomingList);
382 636 return $this->sendSuccess([
383 - 'message' => __('Board stage has been updated', 'fluent-boards')
384 - ], 200);
385 - } catch (\Exception $e) {
386 - return $this->sendError($e->getMessage(), 400);
387 - }
388 - }
389 -
390 - public function rePositionStages(Request $request, $board_id)
391 - {
392 - $incomingList = $request->get('list');
393 - try {
394 - $this->boardService->rePositionStages($board_id, $incomingList);
395 - return $this->sendSuccess([
396 637 'message' => __('Stages Reordered', 'fluent-boards'),
397 638 'updatedStages' => $this->stageService->getLastOneMinuteUpdatedStages($board_id)
398 639 ], 200);
399 640 } catch (\Exception $e) {
@@ -411,9 +652,9 @@
411 652 public function delete($board_id)
412 653 {
413 654 try {
414 655 if (!PermissionManager::isAdmin()) {
415 - throw new \Exception('You do not have permission to delete this board', 400);
656 + throw new \Exception(esc_html__('You do not have permission to delete this board', 'fluent-boards'), 400);
416 657 }
417 658 $this->boardService->deleteBoard($board_id);
418 659
419 660 return $this->sendSuccess([
@@ -431,9 +672,12 @@
431 672
432 673 public function getActivities(Request $request, $board_id)
433 674 {
434 675 try {
435 - $activities = $this->boardService->getActivities($board_id, $request->all());
676 + $activities = $this->boardService->getActivities($board_id, [
677 + 'per_page' => $request->getSafe('per_page', 'intval', 40),
678 + 'page' => $request->getSafe('page', 'intval', 1),
679 + ]);
436 680 return $this->sendSuccess([
437 681 'activities' => $activities,
438 682 ], 200);
439 683 } catch (\Exception $e) {
@@ -502,16 +746,24 @@
502 746 return $returnData;
503 747 }
504 748
505 749 /*
506 - * These are the rest of the admin users who are not in the board
750 + * These are the rest of the Fluent Boards and WordPress admins who are not in the board.
507 751 */
508 - $adminUserIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
752 + $fluentBoardAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
509 753 ->whereNotIn('object_id', $userIds)
510 754 ->get()
511 755 ->pluck('object_id')
512 756 ->toArray();
513 757
758 + $wordPressAdminIds = get_users([
759 + 'capability' => 'manage_options',
760 + 'exclude' => $userIds,
761 + 'fields' => 'ID',
762 + ]);
763 +
764 + $adminUserIds = array_values(array_unique(array_map('intval', array_merge($fluentBoardAdminIds, $wordPressAdminIds))));
765 +
514 766 if ($adminUserIds) {
515 767 $adminUsers = get_users([
516 768 'include' => $adminUserIds,
517 769 ]);
@@ -561,19 +813,25 @@
561 813 }
562 814
563 815 public function addMembersInBoard(Request $request, $board_id)
564 816 {
565 - $memberId = $request->getSafe('memberId');
566 - $isViewerOnly = $request->getSafe('isViewerOnly');
567 - $isAlreadyMember = $this->boardService->isAlreadyMember($board_id, $memberId);
817 + $memberId = $request->getSafe('memberId', 'intval');
818 + $isViewerOnly = $request->getSafe('isViewerOnly', 'sanitize_text_field');
819 + $member = $this->boardService->addMembersInBoard($board_id, $memberId, $isViewerOnly);
568 820
569 - if ($isAlreadyMember) {
821 + if ($member === null) {
570 822 return $this->sendError([
823 + 'message' => __('User not found.', 'fluent-boards'),
824 + ], 404);
825 + }
826 +
827 + if (!$member) {
828 + return $this->sendError([
571 829 'message' => __('User already a member', 'fluent-boards'),
572 - ], 304);
830 + ], 409);
573 831 }
574 - $member = $this->boardService->addMembersInBoard($board_id, $memberId, $isViewerOnly);
575 832
833 +
576 834 return [
577 835 'message' => __('Member added successfully', 'fluent-boards'),
578 836 'member' => Helper::sanitizeUserCollections($member)
579 837 ];
@@ -601,11 +859,11 @@
601 859 }
602 860
603 861 public function searchBoards(Request $request)
604 862 {
605 - $per_page = $request->get('per_page', 10);
606 - $search_input = $request->searchInput . trim('');
607 - $type = $request->type;
863 + $per_page = $request->getSafe('per_page', 'intval', 10);
864 + $search_input = $request->getSafe('searchInput', 'sanitize_text_field', '');
865 + $type = $request->getSafe('type', 'sanitize_text_field', 'to-do');
608 866
609 867 $currentUserId = get_current_user_id();
610 868
611 869 if (PermissionManager::isAdmin($currentUserId)) {
@@ -647,10 +905,13 @@
647 905 * @return
648 906 */
649 907 public function changeStageView($board_id, $stage_id)
650 908 {
909 + $board_id = absint($board_id);
910 + $stage_id = absint($stage_id);
911 +
651 912 try {
652 - $stage = Stage::findOrFail($stage_id);
913 + $stage = $this->findStageOnBoard($stage_id, $board_id);
653 914 $message = __('The stage is made public!', 'fluent-boards');
654 915 $settings = $stage->settings;
655 916
656 917 if (isset($settings['is_public'])) {
@@ -655,9 +916,9 @@
655 916
656 917 if (isset($settings['is_public'])) {
657 918 if ($settings['is_public']) {
658 919 $settings['is_public'] = false;
659 - $message = __('The stage is made admin only!', 'fluent-boards');
920 + $message = __('The stage is made private!', 'fluent-boards');
660 921 } else {
661 922 $settings['is_public'] = true;
662 923 }
663 924 } else {
@@ -676,24 +937,27 @@
676 937 }
677 938
678 939
679 940 /**
680 - * Set board background image or color
941 + * Set or reset board background image/color.
681 942 * @param \FluentBoards\Framework\Http\Request\Request $request
682 943 * @return
683 944 */
684 945 public function setBoardBackground(Request $request, $board_id)
685 946 {
686 - // sanitize and validate image_url
687 - if ($request->image_url) {
947 + $backgroundData = [];
948 + $isResetRequest = $request->getSafe('reset', 'rest_sanitize_boolean');
949 +
950 + if ($isResetRequest) {
951 + $backgroundData = [
952 + 'reset' => true,
953 + ];
954 + } elseif ($request->image_url) {
688 955 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
689 - "id" => 'required',
956 + 'id' => 'required|integer',
690 957 'image_url' => 'required|string|url',
691 958 ]);
692 - }
693 -
694 - // sanitize and validate color
695 - if ($request->color) {
959 + } elseif ($request->color) {
696 960 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
697 961 "id" => 'required',
698 962 'color' => 'required',
699 963 ]);
@@ -701,17 +965,22 @@
701 965
702 966 try {
703 967 if (!$board_id) {
704 968 $errorMessage = __('Board id is required', 'fluent-boards');
705 - throw new \Exception($errorMessage, 400);
969 + throw new \Exception(esc_html($errorMessage), 400);
706 970 }
707 971
972 + if (empty($backgroundData)) {
973 + $errorMessage = __('Background data is required', 'fluent-boards');
974 + throw new \Exception(esc_html($errorMessage), 400);
975 + }
976 +
708 977 return $this->sendSuccess([
709 978 'message' => __('Background updated successfully', 'fluent-boards'),
710 979 'background' => $this->boardService->setBoardBackground($backgroundData, $board_id),
711 980 ]);
712 981 } catch (\Exception $e) {
713 - $this->sendError([$e->getMessage(), 400]);
982 + return $this->sendError($e->getMessage(), 400);
714 983 }
715 984 }
716 985
717 986
@@ -721,15 +990,19 @@
721 990 * @param mixed $stage_slug
722 991 * @return $availablePositions as an array
723 992 * @throws \Exception
724 993 */
725 - public function getStageTaskAvailablePositions($board_id, $stage_id)
994 + public function getStageTaskAvailablePositions(Request $request, $board_id, $stage_id)
726 995 {
727 996 try {
728 997 if ($board_id && $stage_id) {
729 - $availablePositions = $this->boardService->getStageTaskAvailablePositions($board_id, $stage_id);
998 + $taskId = $request->getSafe('task_id', 'intval');
999 + $availablePositions = $this->boardService->getStageTaskAvailablePositions($board_id, $stage_id, $taskId);
730 1000 return $this->sendSuccess([
731 - 'availablePositions' => $availablePositions
1001 + 'availablePositions' => $availablePositions['availablePositions'],
1002 + 'moveTargets' => $availablePositions['moveTargets'],
1003 + 'currentMoveTargetKey' => $availablePositions['currentMoveTargetKey'],
1004 + 'defaultMoveTargetKey' => $availablePositions['defaultMoveTargetKey'],
732 1005 ], 200);
733 1006 } else {
734 1007 $message = '';
735 1008 if (!$board_id) {
@@ -737,12 +1010,12 @@
737 1010 }
738 1011 if (!$stage_id) {
739 1012 $message = 'Stage ';
740 1013 }
741 - throw new \Exception($message . 'is required', 400);
1014 + throw new \Exception(esc_html($message . 'is required'), 400);
742 1015 }
743 1016 } catch (\Exception $e) {
744 - $this->sendError([$e->getMessage(), 400]);
1017 + return $this->sendError($e->getMessage(), 400);
745 1018 }
746 1019 }
747 1020
748 1021 public function getAssociateCrmContacts($board_id)
@@ -751,24 +1024,47 @@
751 1024 $contactAssociatedTasks = Task::with('board')->where('board_id', $board_id)
752 1025 ->whereNotNull('crm_contact_id')
753 1026 ->get();
754 1027
755 - $formattedContacts = Collection::make($contactAssociatedTasks)
756 - ->groupBy('crm_contact_id')
757 - ->map(function ($tasks, $contactId) {
758 - $subscriber = Subscriber::find($contactId);
759 - if (!$subscriber) {
1028 + $tasksByContact = [];
1029 + foreach ($contactAssociatedTasks as $task) {
1030 + $tasksByContact[absint($task->crm_contact_id)][] = $task;
1031 + }
1032 +
1033 + $boardContactIds = Meta::query()
1034 + ->where('object_id', absint($board_id))
1035 + ->where('object_type', Constant::OBJECT_TYPE_BOARD)
1036 + ->whereIn('key', [
1037 + Constant::BOARD_ASSOCIATED_CRM_CONTACT,
1038 + self::LEGACY_BOARD_ASSOCIATED_CRM_CONTACT,
1039 + ])
1040 + ->pluck('value')
1041 + ->toArray();
1042 + $boardContactIds = array_values(array_unique(array_filter(array_map('absint', $boardContactIds))));
1043 +
1044 + $contactIds = array_values(array_unique(array_filter(array_map('absint', array_merge(
1045 + array_keys($tasksByContact),
1046 + $boardContactIds
1047 + )))));
1048 +
1049 + usort($contactIds, function ($firstContactId, $secondContactId) use ($boardContactIds) {
1050 + return (int) in_array($secondContactId, $boardContactIds, true) - (int) in_array($firstContactId, $boardContactIds, true);
1051 + });
1052 +
1053 + $formattedContacts = Collection::make($contactIds)
1054 + ->map(function ($contactId) use ($tasksByContact, $boardContactIds) {
1055 + $contact = Helper::crm_contact($contactId);
1056 + if (!$contact) {
760 1057 return null; // Skip if subscriber not found
761 1058 }
762 1059
763 - return [
764 - 'name' => $subscriber->first_name . ' ' . $subscriber->last_name,
765 - 'photo' => $subscriber->photo,
766 - 'email' => $subscriber->email,
767 - 'crm_contact_id' => $contactId,
768 - 'id' => $contactId,
769 - 'tasks' => $tasks,
770 - ];
1060 + $tasks = $tasksByContact[$contactId] ?? [];
1061 + $contact['name'] = trim(($contact['first_name'] ?? '') . ' ' . ($contact['last_name'] ?? '')) ?: ($contact['full_name'] ?? $contact['email'] ?? '');
1062 + $contact['crm_contact_id'] = $contactId;
1063 + $contact['is_board_contact'] = in_array($contactId, $boardContactIds, true);
1064 + $contact['tasks'] = $tasks;
1065 +
1066 + return $contact;
771 1067 })
772 1068 ->filter()->toArray();
773 1069
774 1070
@@ -789,11 +1085,13 @@
789 1085 'message' => __('Associated Crm Member has been updated', 'fluent-boards'),
790 1086 ], 200);
791 1087 }
792 1088
793 - public function hasDataChanged($board_id)
1089 + public function hasDataChanged(Request $request, $board_id)
794 1090 {
795 - return $this->boardService->hasDataChanged($board_id);
1091 + $includeArchived = filter_var($request->get('include_archived', false), FILTER_VALIDATE_BOOLEAN);
1092 + $since = $request->getSafe('since', 'sanitize_text_field');
1093 + return $this->boardService->hasDataChanged($board_id, $includeArchived, $since);
796 1094 }
797 1095
798 1096 public function createStage(Request $request, $board_id)
799 1097 {
@@ -816,15 +1114,27 @@
816 1114 }
817 1115
818 1116 public function moveAllTasks(Request $request, $board_id)
819 1117 {
820 - $oldStageId = $request->getSafe('oldStageId');
821 - $newStageId = $request->getSafe('newStageId');
1118 + $oldStageId = $request->getSafe('oldStageId', 'intval');
1119 + $newStageId = $request->getSafe('newStageId', 'intval');
822 1120
1121 + if (!$oldStageId || !$newStageId) {
1122 + return $this->sendError(__('Invalid stage IDs provided', 'fluent-boards'), 400);
1123 + }
1124 +
1125 + // Verify stages exist and belong to the board
1126 + $oldStage = Stage::where('id', $oldStageId)->where('board_id', $board_id)->first();
1127 + $newStage = Stage::where('id', $newStageId)->where('board_id', $board_id)->first();
1128 +
1129 + if (!$oldStage || !$newStage) {
1130 + return $this->sendError(__('One or both stages do not exist or do not belong to this board', 'fluent-boards'), 400);
1131 + }
1132 +
823 1133 $updates = $this->stageService->moveAllTasks($oldStageId, $newStageId, $board_id);
824 1134
825 1135 return [
826 - 'message' => __('Tasks has been Moved', 'fluent-boards'),
1136 + 'message' => __('Tasks have been moved', 'fluent-boards'),
827 1137 'updatedTasks' => $updates,
828 1138 ];
829 1139
830 1140 }
@@ -830,35 +1140,70 @@
830 1140 }
831 1141
832 1142 public function archiveAllTasksInStage($board_id, $stage_id)
833 1143 {
834 - $updates = $this->stageService->archiveAllTasksInStage($stage_id);
835 - return [
836 - 'message' => __('Tasks has been archived', 'fluent-boards'),
837 - 'updatedTasks' => $updates,
838 - ];
1144 + $board_id = absint($board_id);
1145 + $stage_id = absint($stage_id);
1146 +
1147 + try {
1148 + $this->findStageOnBoard($stage_id, $board_id);
1149 + $updates = $this->stageService->archiveAllTasksInStage($stage_id, $board_id);
1150 +
1151 + return [
1152 + 'message' => __('Tasks have been archived', 'fluent-boards'),
1153 + 'updatedTasks' => $updates,
1154 + ];
1155 + } catch (\Exception $e) {
1156 + return $this->sendError($e->getMessage(), 400);
1157 + }
839 1158 }
840 1159
841 1160 public function getAssociatedBoards(Request $request, $associated_id)
842 1161 {
843 - $associatedBoards = $this->boardService->getAssociatedBoards($associated_id);
1162 + if (!$this->currentUserCanReadCrmContacts()) {
1163 + return $this->sendError(esc_html__('You do not have permission to view CRM contact boards', 'fluent-boards'), 403);
1164 + }
1165 +
1166 + $associatedId = absint($associated_id);
1167 +
1168 + if (!$associatedId) {
1169 + return $this->sendError(__('Invalid CRM contact', 'fluent-boards'), 400);
1170 + }
1171 +
1172 + $associatedBoards = $this->boardService->getAssociatedBoards($associatedId, get_current_user_id());
1173 +
844 1174 return [
845 1175 'boards' => $associatedBoards,
846 1176 ];
847 1177 }
848 1178
1179 + private function currentUserCanReadCrmContacts()
1180 + {
1181 + $permissionManager = 'FluentCrm\\App\\Services\\PermissionManager';
1182 +
1183 + if (!class_exists($permissionManager)) {
1184 + return false;
1185 + }
1186 +
1187 + return (bool) $permissionManager::currentUserCan('fcrm_read_contacts');
1188 + }
1189 +
849 1190 public function duplicateBoard(Request $request, $board_id)
850 1191 {
851 1192 $boardData = $this->taskSanitizeAndValidate($request->get('board'), [
852 1193 'title' => 'required|string'
853 1194 ]);
1195 +
1196 + $boardData['source_board_id'] = $board_id;
1197 +
854 1198 $isWithLabels = $request->getSafe('isWithLabels');
855 1199 $isWithTasks = $request->getSafe('isWithTasks');
1200 + $isWithTemplates = $request->getSafe('isWithTemplates');
856 1201
857 1202 try {
858 1203 if(!PermissionManager::isAdmin()) {
859 1204 $errorMessage = __('You do not have permission to duplicate board', 'fluent-boards');
860 - throw new \Exception($errorMessage, 400);
1205 + throw new \Exception(esc_html($errorMessage), 400);
861 1206 }
862 1207 //create board
863 1208 $newBoard = $this->boardService->copyBoard($boardData);
864 1209
@@ -869,13 +1214,13 @@
869 1214 $labelMap = $this->labelService->copyLabelsOfBoard($board_id, $newBoard);
870 1215 }
871 1216
872 1217 //stage copy
873 - $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id);
1218 + $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id, $isWithTemplates);
874 1219
875 1220 //copy tasks of selected stages
876 1221 if ($isWithTasks == 'yes') {
877 - $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap);
1222 + $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap,$isWithTemplates);
878 1223 }
879 1224
880 1225 return $this->sendSuccess([
881 1226 'board' => $newBoard,
@@ -887,10 +1232,16 @@
887 1232
888 1233 public function importFromBoard(Request $request, $board_id)
889 1234 {
890 1235 $selectedStages = $request->getSafe('selectedStages');
891 - $position = $request->getSafe('position');
1236 + $position = $request->getSafe('position', 'intval');
892 1237
1238 + // Validate and sanitize selectedStages array
1239 + if (!is_array($selectedStages)) {
1240 + $selectedStages = [$selectedStages];
1241 + }
1242 + $selectedStages = array_filter(array_map('intval', $selectedStages));
1243 +
893 1244 try {
894 1245 $this->stageService->importStagesFromBoard($board_id, $selectedStages, $position);
895 1246
896 1247 return $this->sendSuccess([
@@ -965,7 +1316,184 @@
965 1316 ? 'manager'
966 1317 : ($boardRelation && Arr::has($boardRelation->settings, 'is_viewer_only') && Arr::get($boardRelation->settings, 'is_viewer_only')
967 1318 ? 'viewer'
968 1319 : 'member');
1320 + }
1321 + public function uploadBoardBackground(Request $request,$board_id)
1322 + {
1323 + $file = Arr::get($request->files(), 'file')->toArray();
1324 + (new \FluentBoards\App\Services\UploadService)->validateFile($file);
1325 +
1326 + $uploadInfo = UploadService::handleFileUpload( $request->files(), $board_id);
1327 +
1328 + $fileData = $uploadInfo[0];
1329 + $initialDataData = [
1330 + 'type' => 'url',
1331 + 'url' => '',
1332 + 'name' => '',
1333 + 'size' => 0,
1334 + ];
1335 +
1336 + $attachData = array_merge($initialDataData, $fileData);
1337 + $UrlMeta = [];
1338 + if($attachData['type'] == 'url') {
1339 + $UrlMeta = RemoteUrlParser::parse($attachData['url']);
1340 + }
1341 + $uid = wp_generate_uuid4();
1342 + $fileUploadedData = new Attachment();
1343 + $fileUploadedData->object_id = $board_id;
1344 + $fileUploadedData->object_type = Constant::BOARD_BACKGROUND_IMAGE;
1345 + $fileUploadedData->attachment_type = $attachData['type'];
1346 + $fileUploadedData->title = (new TaskService())->setTitle($attachData['type'], $attachData['name'], $UrlMeta);
1347 + $fileUploadedData->file_path = $attachData['type'] != 'url' ? $attachData['file'] : null;
1348 + $fileUploadedData->full_url = esc_url($attachData['url']);
1349 + $fileUploadedData->file_size = $attachData['size'];
1350 + $fileUploadedData->settings = $attachData['type'] == 'url' ? [
1351 + 'meta' => $UrlMeta
1352 + ] : '';
1353 + $fileUploadedData->driver = 'local';
1354 + $fileUploadedData->file_hash = md5($uid . wp_rand(0, 1000));
1355 + $fileUploadedData->save();
1356 + if(!!defined('FLUENT_BOARDS_PRO_VERSION')) {
1357 + $mediaData = (new AttachmentService())->processMediaData($fileData, $file);
1358 + $fileUploadedData['driver'] = $mediaData['driver'];
1359 + $fileUploadedData['file_path'] = $mediaData['file_path'];
1360 + $fileUploadedData['full_url'] = $mediaData['full_url'];
1361 + $fileUploadedData->save();
1362 + }
1363 +
1364 + $board = Board::find($board_id);
1365 + $oldBackground = $board->background;
1366 + $publicUrl = (new CommentService())->createPublicUrl($fileUploadedData, $board_id);
1367 + $background = [
1368 + 'color' => null,
1369 + 'id' => $fileUploadedData->id,
1370 + 'image_url' => $publicUrl,
1371 + 'is_image' => true,
1372 + ];
1373 + $board->background = $background;
1374 + $board->save();
1375 + do_action('fluent_boards/board_background_updated', $board_id, $oldBackground);
1376 +
1377 + return $this->sendSuccess([
1378 + 'message' => __('Background updated successfully', 'fluent-boards'),
1379 + 'background' => $board->background,
1380 + ]);
1381 + }
1382 +
1383 + public function getPinnedBoards()
1384 + {
1385 + $pinnedBoards = $this->boardService->getPinnedBoards();
1386 +
1387 + return $this->sendSuccess([
1388 + 'pinnedBoards' => $pinnedBoards,
1389 + ], 200);
1390 + }
1391 +
1392 + public function pinBoard($boardId)
1393 + {
1394 + $this->boardService->pinBoard($boardId);
1395 +
1396 + return $this->sendSuccess([
1397 + 'message' => __('The Board has been pinned', 'fluent-boards'),
1398 + ], 200);
1399 + }
1400 +
1401 + public function unpinBoard($boardId)
1402 + {
1403 + $remove = $this->boardService->unpinBoard($boardId);
1404 +
1405 + if (!$remove) {
1406 + return $this->sendError([
1407 + 'message' => __('Board is not pinned', 'fluent-boards'),
1408 + ], 400);
1409 + }
1410 +
1411 + return $this->sendSuccess([
1412 + 'message' => __('Board is removed from pinned boards', 'fluent-boards'),
1413 + ], 200);
1414 + }
1415 +
1416 + public function getBoardFolder($board_id)
1417 + {
1418 + try {
1419 + $folder = $this->boardService->getBoardFolder($board_id);
1420 + return $this->sendSuccess([
1421 + 'folder' => $folder,
1422 + ], 200);
1423 + } catch (\Exception $e) {
1424 + return $this->sendError($e->getMessage(), 400);
1425 + }
1426 + }
1427 +
1428 + public function getBoardMenuItems($board_id)
1429 + {
1430 + try {
1431 + $menuItems = (new BoardMenuHandler())->getMenuItems($board_id);
1432 +
1433 + return $this->sendSuccess([
1434 + 'menu_items' => $menuItems
1435 + ], 200);
1436 + } catch (\Exception $e) {
1437 + return $this->sendError($e->getMessage(), 500);
1438 + }
1439 + }
1440 +
1441 + public function getPublicAccessSettings($board_id)
1442 + {
1443 + $board_id = absint($board_id);
1444 + $board = Board::findOrFail($board_id);
1445 +
1446 + $enabled = (bool) $board->getMetaByKey('public_access_enabled');
1447 + $shortcode = $enabled ? '[fluent_board_public id="' . $board_id . '"]' : '';
1448 +
1449 + return $this->sendSuccess([
1450 + 'enabled' => $enabled,
1451 + 'shortcode' => $shortcode,
1452 + ], 200);
1453 + }
1454 +
1455 + public function togglePublicAccess(Request $request, $board_id)
1456 + {
1457 + $board_id = absint($board_id);
1458 + $board = Board::findOrFail($board_id);
1459 +
1460 + $enabled = filter_var(
1461 + $request->getSafe('enabled', 'sanitize_text_field', false),
1462 + FILTER_VALIDATE_BOOLEAN
1463 + );
1464 +
1465 + $board->updateMeta('public_access_enabled', $enabled ? '1' : '');
1466 +
1467 + $shortcode = $enabled ? '[fluent_board_public id="' . $board_id . '"]' : '';
1468 +
1469 + return $this->sendSuccess([
1470 + 'message' => $enabled
1471 + ? __('Public access has been enabled', 'fluent-boards')
1472 + : __('Public access has been disabled', 'fluent-boards'),
1473 + 'enabled' => $enabled,
1474 + 'shortcode' => $shortcode,
1475 + ], 200);
1476 + }
1477 +
1478 + /**
1479 + * Resolve a stage only when it belongs to the requested board.
1480 + *
1481 + * @param int $stageId
1482 + * @param int $boardId
1483 + * @return Stage
1484 + * @throws \Exception
1485 + */
1486 + private function findStageOnBoard($stageId, $boardId)
1487 + {
1488 + $stage = Stage::where('id', absint($stageId))
1489 + ->where('board_id', absint($boardId))
1490 + ->first();
1491 +
1492 + if (!$stage) {
1493 + throw new \Exception(esc_html__('Stage not found', 'fluent-boards'));
1494 + }
1495 +
1496 + return $stage;
969 1497 }
970 1498
971 1499 }