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 +666 -135 1.35trunk 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',
@@ -178,9 +287,9 @@
178 287
179 288 $taskData = null;
180 289 if ($request->get('task')) {
181 290 $taskData = $this->taskSanitizeAndValidate($request->get('task'), [
182 - 'title' => 'required|string'
291 + 'title' => 'required|string',
183 292 ]);
184 293 }
185 294
186 295 $board = $this->boardService->createBoard($boardData);
@@ -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) {
@@ -478,8 +722,9 @@
478 722
479 723 $formattedUsers[] = [
480 724 'ID' => $user->ID,
481 725 'display_name' => $name,
726 + 'user_login' => $user->user_login,
482 727 'email' => $user->user_email,
483 728 'photo' => fluent_boards_user_avatar($user->user_email, $name),
484 729 'role' => $this->boardUserRole($boardRelation),
485 730 'is_super' => in_array($user->ID, $superAdminIds),
@@ -501,16 +746,24 @@
501 746 return $returnData;
502 747 }
503 748
504 749 /*
505 - * 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.
506 751 */
507 - $adminUserIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
752 + $fluentBoardAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
508 753 ->whereNotIn('object_id', $userIds)
509 754 ->get()
510 755 ->pluck('object_id')
511 756 ->toArray();
512 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 +
513 766 if ($adminUserIds) {
514 767 $adminUsers = get_users([
515 768 'include' => $adminUserIds,
516 769 ]);
@@ -560,19 +813,25 @@
560 813 }
561 814
562 815 public function addMembersInBoard(Request $request, $board_id)
563 816 {
564 - $memberId = $request->getSafe('memberId');
565 - $isViewerOnly = $request->getSafe('isViewerOnly');
566 - $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);
567 820
568 - if ($isAlreadyMember) {
821 + if ($member === null) {
569 822 return $this->sendError([
823 + 'message' => __('User not found.', 'fluent-boards'),
824 + ], 404);
825 + }
826 +
827 + if (!$member) {
828 + return $this->sendError([
570 829 'message' => __('User already a member', 'fluent-boards'),
571 - ], 304);
830 + ], 409);
572 831 }
573 - $member = $this->boardService->addMembersInBoard($board_id, $memberId, $isViewerOnly);
574 832
833 +
575 834 return [
576 835 'message' => __('Member added successfully', 'fluent-boards'),
577 836 'member' => Helper::sanitizeUserCollections($member)
578 837 ];
@@ -600,11 +859,11 @@
600 859 }
601 860
602 861 public function searchBoards(Request $request)
603 862 {
604 - $per_page = $request->get('per_page', 10);
605 - $search_input = $request->searchInput . trim('');
606 - $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');
607 866
608 867 $currentUserId = get_current_user_id();
609 868
610 869 if (PermissionManager::isAdmin($currentUserId)) {
@@ -646,10 +905,13 @@
646 905 * @return
647 906 */
648 907 public function changeStageView($board_id, $stage_id)
649 908 {
909 + $board_id = absint($board_id);
910 + $stage_id = absint($stage_id);
911 +
650 912 try {
651 - $stage = Stage::findOrFail($stage_id);
913 + $stage = $this->findStageOnBoard($stage_id, $board_id);
652 914 $message = __('The stage is made public!', 'fluent-boards');
653 915 $settings = $stage->settings;
654 916
655 917 if (isset($settings['is_public'])) {
@@ -654,9 +916,9 @@
654 916
655 917 if (isset($settings['is_public'])) {
656 918 if ($settings['is_public']) {
657 919 $settings['is_public'] = false;
658 - $message = __('The stage is made admin only!', 'fluent-boards');
920 + $message = __('The stage is made private!', 'fluent-boards');
659 921 } else {
660 922 $settings['is_public'] = true;
661 923 }
662 924 } else {
@@ -675,24 +937,27 @@
675 937 }
676 938
677 939
678 940 /**
679 - * Set board background image or color
941 + * Set or reset board background image/color.
680 942 * @param \FluentBoards\Framework\Http\Request\Request $request
681 943 * @return
682 944 */
683 945 public function setBoardBackground(Request $request, $board_id)
684 946 {
685 - // sanitize and validate image_url
686 - 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) {
687 955 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
688 - "id" => 'required',
956 + 'id' => 'required|integer',
689 957 'image_url' => 'required|string|url',
690 958 ]);
691 - }
692 -
693 - // sanitize and validate color
694 - if ($request->color) {
959 + } elseif ($request->color) {
695 960 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
696 961 "id" => 'required',
697 962 'color' => 'required',
698 963 ]);
@@ -700,17 +965,22 @@
700 965
701 966 try {
702 967 if (!$board_id) {
703 968 $errorMessage = __('Board id is required', 'fluent-boards');
704 - throw new \Exception($errorMessage, 400);
969 + throw new \Exception(esc_html($errorMessage), 400);
705 970 }
706 971
972 + if (empty($backgroundData)) {
973 + $errorMessage = __('Background data is required', 'fluent-boards');
974 + throw new \Exception(esc_html($errorMessage), 400);
975 + }
976 +
707 977 return $this->sendSuccess([
708 978 'message' => __('Background updated successfully', 'fluent-boards'),
709 979 'background' => $this->boardService->setBoardBackground($backgroundData, $board_id),
710 980 ]);
711 981 } catch (\Exception $e) {
712 - $this->sendError([$e->getMessage(), 400]);
982 + return $this->sendError($e->getMessage(), 400);
713 983 }
714 984 }
715 985
716 986
@@ -720,15 +990,19 @@
720 990 * @param mixed $stage_slug
721 991 * @return $availablePositions as an array
722 992 * @throws \Exception
723 993 */
724 - public function getStageTaskAvailablePositions($board_id, $stage_id)
994 + public function getStageTaskAvailablePositions(Request $request, $board_id, $stage_id)
725 995 {
726 996 try {
727 997 if ($board_id && $stage_id) {
728 - $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);
729 1000 return $this->sendSuccess([
730 - 'availablePositions' => $availablePositions
1001 + 'availablePositions' => $availablePositions['availablePositions'],
1002 + 'moveTargets' => $availablePositions['moveTargets'],
1003 + 'currentMoveTargetKey' => $availablePositions['currentMoveTargetKey'],
1004 + 'defaultMoveTargetKey' => $availablePositions['defaultMoveTargetKey'],
731 1005 ], 200);
732 1006 } else {
733 1007 $message = '';
734 1008 if (!$board_id) {
@@ -736,12 +1010,12 @@
736 1010 }
737 1011 if (!$stage_id) {
738 1012 $message = 'Stage ';
739 1013 }
740 - throw new \Exception($message . 'is required', 400);
1014 + throw new \Exception(esc_html($message . 'is required'), 400);
741 1015 }
742 1016 } catch (\Exception $e) {
743 - $this->sendError([$e->getMessage(), 400]);
1017 + return $this->sendError($e->getMessage(), 400);
744 1018 }
745 1019 }
746 1020
747 1021 public function getAssociateCrmContacts($board_id)
@@ -750,24 +1024,47 @@
750 1024 $contactAssociatedTasks = Task::with('board')->where('board_id', $board_id)
751 1025 ->whereNotNull('crm_contact_id')
752 1026 ->get();
753 1027
754 - $formattedContacts = Collection::make($contactAssociatedTasks)
755 - ->groupBy('crm_contact_id')
756 - ->map(function ($tasks, $contactId) {
757 - $subscriber = Subscriber::find($contactId);
758 - 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) {
759 1057 return null; // Skip if subscriber not found
760 1058 }
761 1059
762 - return [
763 - 'name' => $subscriber->first_name . ' ' . $subscriber->last_name,
764 - 'photo' => $subscriber->photo,
765 - 'email' => $subscriber->email,
766 - 'crm_contact_id' => $contactId,
767 - 'id' => $contactId,
768 - 'tasks' => $tasks,
769 - ];
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;
770 1067 })
771 1068 ->filter()->toArray();
772 1069
773 1070
@@ -788,11 +1085,13 @@
788 1085 'message' => __('Associated Crm Member has been updated', 'fluent-boards'),
789 1086 ], 200);
790 1087 }
791 1088
792 - public function hasDataChanged($board_id)
1089 + public function hasDataChanged(Request $request, $board_id)
793 1090 {
794 - 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);
795 1094 }
796 1095
797 1096 public function createStage(Request $request, $board_id)
798 1097 {
@@ -797,8 +1096,9 @@
797 1096 public function createStage(Request $request, $board_id)
798 1097 {
799 1098 $stageData = $this->stageSanitizeAndValidate($request->all(), [
800 1099 'title' => 'required|string',
1100 + 'position' => 'nullable|numeric'
801 1101 ]);
802 1102
803 1103 $board = Board::find($board_id);
804 1104 $stage = $this->stageService->createStage($stageData, $board_id);
@@ -814,15 +1114,27 @@
814 1114 }
815 1115
816 1116 public function moveAllTasks(Request $request, $board_id)
817 1117 {
818 - $oldStageId = $request->getSafe('oldStageId');
819 - $newStageId = $request->getSafe('newStageId');
1118 + $oldStageId = $request->getSafe('oldStageId', 'intval');
1119 + $newStageId = $request->getSafe('newStageId', 'intval');
820 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 +
821 1133 $updates = $this->stageService->moveAllTasks($oldStageId, $newStageId, $board_id);
822 1134
823 1135 return [
824 - 'message' => __('Tasks has been Moved', 'fluent-boards'),
1136 + 'message' => __('Tasks have been moved', 'fluent-boards'),
825 1137 'updatedTasks' => $updates,
826 1138 ];
827 1139
828 1140 }
@@ -828,35 +1140,70 @@
828 1140 }
829 1141
830 1142 public function archiveAllTasksInStage($board_id, $stage_id)
831 1143 {
832 - $updates = $this->stageService->archiveAllTasksInStage($stage_id);
833 - return [
834 - 'message' => __('Tasks has been archived', 'fluent-boards'),
835 - 'updatedTasks' => $updates,
836 - ];
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 + }
837 1158 }
838 1159
839 1160 public function getAssociatedBoards(Request $request, $associated_id)
840 1161 {
841 - $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 +
842 1174 return [
843 1175 'boards' => $associatedBoards,
844 1176 ];
845 1177 }
846 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 +
847 1190 public function duplicateBoard(Request $request, $board_id)
848 1191 {
849 1192 $boardData = $this->taskSanitizeAndValidate($request->get('board'), [
850 1193 'title' => 'required|string'
851 1194 ]);
1195 +
1196 + $boardData['source_board_id'] = $board_id;
1197 +
852 1198 $isWithLabels = $request->getSafe('isWithLabels');
853 1199 $isWithTasks = $request->getSafe('isWithTasks');
1200 + $isWithTemplates = $request->getSafe('isWithTemplates');
854 1201
855 1202 try {
856 1203 if(!PermissionManager::isAdmin()) {
857 1204 $errorMessage = __('You do not have permission to duplicate board', 'fluent-boards');
858 - throw new \Exception($errorMessage, 400);
1205 + throw new \Exception(esc_html($errorMessage), 400);
859 1206 }
860 1207 //create board
861 1208 $newBoard = $this->boardService->copyBoard($boardData);
862 1209
@@ -867,13 +1214,13 @@
867 1214 $labelMap = $this->labelService->copyLabelsOfBoard($board_id, $newBoard);
868 1215 }
869 1216
870 1217 //stage copy
871 - $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id);
1218 + $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id, $isWithTemplates);
872 1219
873 1220 //copy tasks of selected stages
874 1221 if ($isWithTasks == 'yes') {
875 - $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap);
1222 + $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap,$isWithTemplates);
876 1223 }
877 1224
878 1225 return $this->sendSuccess([
879 1226 'board' => $newBoard,
@@ -885,11 +1232,18 @@
885 1232
886 1233 public function importFromBoard(Request $request, $board_id)
887 1234 {
888 1235 $selectedStages = $request->getSafe('selectedStages');
1236 + $position = $request->getSafe('position', 'intval');
889 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 +
890 1244 try {
891 - $this->stageService->importStagesFromBoard($board_id, $selectedStages);
1245 + $this->stageService->importStagesFromBoard($board_id, $selectedStages, $position);
892 1246
893 1247 return $this->sendSuccess([
894 1248 'message' => __('Import successfully', 'fluent-boards'),
895 1249 ], 200);
@@ -962,7 +1316,184 @@
962 1316 ? 'manager'
963 1317 : ($boardRelation && Arr::has($boardRelation->settings, 'is_viewer_only') && Arr::get($boardRelation->settings, 'is_viewer_only')
964 1318 ? 'viewer'
965 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;
966 1497 }
967 1498
968 1499 }