PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.1.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.1.0
2.1.0 2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 1.40 1.41 All 42 releases
← All changes | app/Http/Controllers/BoardController.php +698 -135 1.412.1.0 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,46 @@
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 +
354 + $this->validateRequestedLabelPresets($request->get('labels'));
355 +
233 356 $board = $this->boardService->createBoard($boardData);
234 - $this->labelService->createDefaultLabel($board->id);
357 + $this->createBoardLabelsFromRequest($request, $board->id);
358 + $this->addBoardMembersFromRequest($request, $board->id);
235 359 $type = ucfirst($boardData['type']);
360 + $stages = $request->get('stages');
361 + $sanitizedStages = [];
236 362
363 + if (is_array($stages) && !empty($stages)) {
364 + foreach ($stages as $stage) {
365 + $sanitizedStages[] = $this->stageSanitizeAndValidate($stage, [
366 + 'title' => 'required|string',
367 + 'slug' => 'nullable|string',
368 + 'position' => 'nullable|numeric'
369 + ]);
370 + }
371 + }
372 +
237 373 if (isset($boardData['type']) && $boardData['type'] == 'roadmap') {
238 - $this->stageService->createRoadmapStages($board, $request->get('stages'));
374 + $this->stageService->createRoadmapStages($board, $sanitizedStages);
375 + } elseif (!empty($sanitizedStages)) {
376 + $this->stageService->createStages($board, $sanitizedStages);
239 377 } else {
240 378 $this->stageService->createDefaultStages($board);
241 379 }
242 380
@@ -246,37 +384,151 @@
246 384 }
247 385
248 386 do_action('fluent_boards/board_created', $board);
249 387
388 +
389 + if ($folderId) {
390 + $folderService->addBoardToFolder($folderId, [$board->id]);
391 + }
392 +
250 393 $message = __('Board has been created successfully', 'fluent-boards');
251 394
252 - return $this->sendSuccess([
395 + return $this->send([
253 396 'message' => $message,
254 397 'board' => $board,
255 398 ], 201);
256 399 } catch (\Exception $e) {
257 - return $this->sendError($e->getMessage(), 400);
400 + return $this->sendError([
401 + 'message' => $e->getMessage()
402 + ]);
258 403 }
259 404 }
260 405
406 + private function sanitizeCreateBoardBackground($background)
407 + {
408 + if (!is_array($background) || empty($background['id'])) {
409 + return '';
410 + }
411 +
412 + $backgroundId = sanitize_text_field($background['id']);
413 +
414 + // Only accept ids from the curated solid/gradient palettes and always
415 + // persist the canonical value from the constant (never the client-supplied
416 + // color) so arbitrary CSS can't be stored and later rendered into a style.
417 + $allowedBackgrounds = [];
418 + foreach (array_merge(
419 + Constant::BOARD_BACKGROUND_DEFAULT_SOLID_COLORS,
420 + Constant::BOARD_BACKGROUND_DEFAULT_GRADIENT_COLORS
421 + ) as $option) {
422 + if (isset($option['id'], $option['value'])) {
423 + $allowedBackgrounds[$option['id']] = $option['value'];
424 + }
425 + }
426 +
427 + if (!isset($allowedBackgrounds[$backgroundId])) {
428 + return '';
429 + }
430 +
431 + return [
432 + 'id' => $backgroundId,
433 + 'color' => $allowedBackgrounds[$backgroundId],
434 + 'is_image' => false,
435 + 'image_url' => null,
436 + ];
437 + }
438 +
439 + private function createBoardLabelsFromRequest(Request $request, $boardId)
440 + {
441 + $labels = $request->get('labels');
442 +
443 + if (!is_array($labels)) {
444 + $this->labelService->createDefaultLabel($boardId);
445 + return;
446 + }
447 +
448 + foreach ($labels as $label) {
449 + $labelData = Helper::sanitizeLabel((array) $label);
450 +
451 + if (empty($labelData['label']) && empty($labelData['bg_color']) && empty($labelData['color_preset'])) {
452 + continue;
453 + }
454 +
455 + $labelPayload = [
456 + 'label' => $labelData['label'] ?? '',
457 + 'bg_color' => $labelData['bg_color'] ?? '#f3f4f6',
458 + 'color' => $labelData['color'] ?? '#1B2533',
459 + ];
460 +
461 + if (array_key_exists('color_preset', $labelData)) {
462 + $labelPayload['color_preset'] = $labelData['color_preset'];
463 + }
464 +
465 + $this->labelService->createLabel($labelPayload, $boardId);
466 + }
467 + }
468 +
469 + /**
470 + * Reject unsupported label preset ids before creating any board records.
471 + *
472 + * @param mixed $labels
473 + * @return void
474 + * @throws \Exception
475 + */
476 + private function validateRequestedLabelPresets($labels)
477 + {
478 + if (!is_array($labels)) {
479 + return;
480 + }
481 +
482 + foreach ($labels as $label) {
483 + $labelData = Helper::sanitizeLabel((array) $label);
484 + $presetId = $labelData[Constant::LABEL_COLOR_PRESET_SETTING] ?? null;
485 +
486 + if ($presetId === null || $presetId === '') {
487 + continue;
488 + }
489 +
490 + if (!is_string($presetId) || !Constant::getLabelColorPreset($presetId)) {
491 + throw new \Exception(esc_html__('Invalid label color preset', 'fluent-boards'));
492 + }
493 + }
494 + }
495 +
496 + private function addBoardMembersFromRequest(Request $request, $boardId)
497 + {
498 + $memberIds = $request->get('member_ids');
499 +
500 + if (!is_array($memberIds)) {
501 + return;
502 + }
503 +
504 + $memberIds = array_filter(array_unique(array_map('intval', $memberIds)));
505 + $currentUserId = get_current_user_id();
506 +
507 + foreach ($memberIds as $memberId) {
508 + if ($memberId === $currentUserId) {
509 + continue;
510 + }
511 +
512 + $this->boardService->addMembersInBoard($boardId, $memberId);
513 + }
514 + }
515 +
516 + /**
517 + * Get archived stages for a board with optional pagination and archive actor metadata.
518 + */
261 519 public function getArchivedStage(Request $request, $board_id)
262 520 {
263 521 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 - }
522 + $board_id = absint($board_id);
523 + $sanitizedParams = [
524 + 'noPagination' => $request->getSafe('noPagination', 'boolval', false),
525 + 'per_page' => $request->getSafe('per_page', 'intval', 30),
526 + 'page' => $request->getSafe('page', 'intval', 1),
527 + ];
278 528
529 + $stages = $this->stageService->getArchivedStages($sanitizedParams, $board_id);
530 +
279 531 return $this->sendSuccess([
280 532 'stages' => $stages,
281 533 ], 200);
282 534 } catch (\Exception $e) {
@@ -283,17 +535,33 @@
283 535 return $this->sendError($e->getMessage(), 404);
284 536 }
285 537 }
286 538
287 - public function find($board_id)
539 + public function find(Request $request, $board_id)
288 540 {
289 541 $board = Board::findOrFail($board_id);
542 + $board->description = DescriptionMarkdownConverter::normalize($board->description);
543 + $includeArchived = filter_var($request->get('include_archived', false), FILTER_VALIDATE_BOOLEAN);
290 544 $board->background = maybe_unserialize($board->background);
291 545 $board->createdOn = $board->created_at->format('Y-m-d');
292 546
293 - $board->load(['users', 'stages', 'labels', 'owner']);
547 + $board->load(['users', 'labels', 'owner']);
294 548
549 + if ($includeArchived) {
550 + $board->stages = Stage::where('board_id', $board_id)
551 + ->orderBy('position', 'asc')
552 + ->get();
553 + } else {
554 + $board->load('stages');
555 + }
556 +
295 557 if (defined('FLUENT_BOARDS_PRO')){
558 + $customFiledPositionMeta = $board->getMetaByKey('custom_field_positions');
559 + if(!$customFiledPositionMeta) {
560 + (new CustomFieldService())->reIndexCustomFieldPositions($board_id);
561 + $board->updateMeta('custom_field_positions', 'yes');
562 + }
563 +
296 564 $board->load(['customFields']);
297 565 }
298 566
299 567 $this->boardService->updateRecentBoards($board_id);
@@ -299,21 +567,35 @@
299 567 $this->boardService->updateRecentBoards($board_id);
300 568
301 569 $board->labelColor = Constant::TRELLO_COLOR_MAP;
302 570 $board->labelColorText = Constant::TEXT_COLOR_MAP;
571 + $board->labelColorPresets = Constant::LABEL_COLOR_PRESETS;
303 572
304 573 $board->users = Helper::sanitizeUserCollections($board->users);
305 574 $board->owner = Helper::sanitizeUserCollections($board->owner);
306 575
576 + $board->is_pinned = $this->boardService->isPinned($board->id);
577 +
307 578 $board = apply_filters('fluent_boards/board_find', $board);
308 579
309 580 return [
310 - 'board' => $board
581 + 'board' => $board,
582 + 'synced_at' => current_time('mysql')
311 583 ];
312 584 }
313 585
314 586 public function update(Request $request, $board_id)
315 587 {
588 + // Board identity (title/description) is manager-only. This action shares the
589 + // `update` name with CommentController@update under the same policy group, so the
590 + // guard lives here rather than in a SingleBoardPolicy::update() method that would
591 + // also block ordinary members from editing their own comments.
592 + if (!PermissionManager::isBoardManager(absint($board_id))) {
593 + return $this->sendError([
594 + 'message' => __('You do not have permission to edit this board.', 'fluent-boards'),
595 + ], 403);
596 + }
597 +
316 598 $boardData = $this->boardSanitizeAndValidate($request->only(['title', 'description']), [
317 599 'title' => 'required|string',
318 600 'description' => 'nullable|string',
319 601 ]);
@@ -318,8 +600,9 @@
318 600 'description' => 'nullable|string',
319 601 ]);
320 602
321 603 $board = Board::findOrFail($board_id);
604 + $boardData['description'] = DescriptionMarkdownConverter::normalize($boardData['description']);
322 605
323 606 $oldBoard = clone $board;
324 607 $board->fill($boardData);
325 608 $board->save();
@@ -326,21 +609,23 @@
326 609
327 610 do_action('fluent_boards/board_updated', $board, $oldBoard);
328 611
329 612 return [
330 - 'stages' => $board->stages()->get(),
331 613 'message' => __('Board has been updated', 'fluent-boards'),
332 614 'board' => $board,
615 + 'stages' => $board->stages()->get(),
333 616 ];
334 617 }
335 618
336 619 public function archiveStage($board_id, $stage_id)
337 620 {
621 + $board_id = absint($board_id);
622 + $stage_id = absint($stage_id);
623 +
338 624 try {
339 - $stage = Stage::findOrFail($stage_id);
340 - $board = Board::findOrFail($stage->board_id);
625 + $stage = $this->findStageOnBoard($stage_id, $board_id);
341 626
342 - $updatedStage = $this->boardService->archiveStage($board->id, $stage);
627 + $updatedStage = $this->boardService->archiveStage($board_id, $stage);
343 628
344 629 return $this->sendSuccess([
345 630 'updatedStage' => $updatedStage,
346 631 'message' => __('Stage has been archived', 'fluent-boards'),
@@ -351,18 +636,20 @@
351 636 }
352 637
353 638 public function restoreStage($board_id, $stage_id)
354 639 {
640 + $board_id = absint($board_id);
641 + $stage_id = absint($stage_id);
642 +
355 643 try {
356 - $stage = Stage::findOrFail($stage_id);
357 - $board = Board::findOrFail($board_id);
644 + $stage = $this->findStageOnBoard($stage_id, $board_id);
358 645
359 - $updatedStage = $this->boardService->restoreStage($board->id, $stage);
646 + $updatedStage = $this->boardService->restoreStage($board_id, $stage);
360 647
361 648 return $this->sendSuccess([
362 - 'success' => true,
649 + 'success' => true,
363 650 'updatedStage' => $updatedStage,
364 - 'message' => __('Stage has been restored', 'fluent-boards')
651 + 'message' => __('Stage has been restored', 'fluent-boards')
365 652 ], 200);
366 653 } catch (\Exception $e) {
367 654 return $this->sendError($e->getMessage(), 400);
368 655 }
@@ -368,32 +655,22 @@
368 655 }
369 656 }
370 657
371 658
372 - public function changePositionOfStage(Request $request, $board_id)
659 + public function repositionStages(Request $request, $board_id)
373 660 {
374 - $changeData = $this->boardSanitizeAndValidate($request->only(['fromPosition', 'toPosition']), [
375 - 'fromPosition' => 'required',
376 - 'toPosition' => 'required',
377 - ]);
378 -
661 + $incomingList = $request->get('list');
662 + if (!is_array($incomingList)) {
663 + $incomingList = [];
664 + }
665 + $incomingList = array_map('intval', $incomingList);
379 666 try {
380 - $this->boardService->changePositionOfStage($board_id, $changeData);
667 + foreach ($incomingList as $stageId) {
668 + $this->findStageOnBoard($stageId, $board_id);
669 + }
381 670
671 + $this->boardService->repositionStages($board_id, $incomingList);
382 672 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 673 'message' => __('Stages Reordered', 'fluent-boards'),
397 674 'updatedStages' => $this->stageService->getLastOneMinuteUpdatedStages($board_id)
398 675 ], 200);
399 676 } catch (\Exception $e) {
@@ -411,9 +688,9 @@
411 688 public function delete($board_id)
412 689 {
413 690 try {
414 691 if (!PermissionManager::isAdmin()) {
415 - throw new \Exception('You do not have permission to delete this board', 400);
692 + throw new \Exception(esc_html__('You do not have permission to delete this board', 'fluent-boards'), 400);
416 693 }
417 694 $this->boardService->deleteBoard($board_id);
418 695
419 696 return $this->sendSuccess([
@@ -431,9 +708,12 @@
431 708
432 709 public function getActivities(Request $request, $board_id)
433 710 {
434 711 try {
435 - $activities = $this->boardService->getActivities($board_id, $request->all());
712 + $activities = $this->boardService->getActivities($board_id, [
713 + 'per_page' => $request->getSafe('per_page', 'intval', 40),
714 + 'page' => $request->getSafe('page', 'intval', 1),
715 + ]);
436 716 return $this->sendSuccess([
437 717 'activities' => $activities,
438 718 ], 200);
439 719 } catch (\Exception $e) {
@@ -478,9 +758,8 @@
478 758
479 759 $formattedUsers[] = [
480 760 'ID' => $user->ID,
481 761 'display_name' => $name,
482 - 'user_login' => $user->user_login,
483 762 'email' => $user->user_email,
484 763 'photo' => fluent_boards_user_avatar($user->user_email, $name),
485 764 'role' => $this->boardUserRole($boardRelation),
486 765 'is_super' => in_array($user->ID, $superAdminIds),
@@ -502,16 +781,24 @@
502 781 return $returnData;
503 782 }
504 783
505 784 /*
506 - * These are the rest of the admin users who are not in the board
785 + * These are the rest of the Fluent Boards and WordPress admins who are not in the board.
507 786 */
508 - $adminUserIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
787 + $fluentBoardAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
509 788 ->whereNotIn('object_id', $userIds)
510 789 ->get()
511 790 ->pluck('object_id')
512 791 ->toArray();
513 792
793 + $wordPressAdminIds = get_users([
794 + 'capability' => 'manage_options',
795 + 'exclude' => $userIds,
796 + 'fields' => 'ID',
797 + ]);
798 +
799 + $adminUserIds = array_values(array_unique(array_map('intval', array_merge($fluentBoardAdminIds, $wordPressAdminIds))));
800 +
514 801 if ($adminUserIds) {
515 802 $adminUsers = get_users([
516 803 'include' => $adminUserIds,
517 804 ]);
@@ -561,19 +848,25 @@
561 848 }
562 849
563 850 public function addMembersInBoard(Request $request, $board_id)
564 851 {
565 - $memberId = $request->getSafe('memberId');
566 - $isViewerOnly = $request->getSafe('isViewerOnly');
567 - $isAlreadyMember = $this->boardService->isAlreadyMember($board_id, $memberId);
852 + $memberId = $request->getSafe('memberId', 'intval');
853 + $isViewerOnly = $request->getSafe('isViewerOnly', 'sanitize_text_field');
854 + $member = $this->boardService->addMembersInBoard($board_id, $memberId, $isViewerOnly);
568 855
569 - if ($isAlreadyMember) {
856 + if ($member === null) {
570 857 return $this->sendError([
858 + 'message' => __('User not found.', 'fluent-boards'),
859 + ], 404);
860 + }
861 +
862 + if (!$member) {
863 + return $this->sendError([
571 864 'message' => __('User already a member', 'fluent-boards'),
572 - ], 304);
865 + ], 409);
573 866 }
574 - $member = $this->boardService->addMembersInBoard($board_id, $memberId, $isViewerOnly);
575 867
868 +
576 869 return [
577 870 'message' => __('Member added successfully', 'fluent-boards'),
578 871 'member' => Helper::sanitizeUserCollections($member)
579 872 ];
@@ -601,11 +894,11 @@
601 894 }
602 895
603 896 public function searchBoards(Request $request)
604 897 {
605 - $per_page = $request->get('per_page', 10);
606 - $search_input = $request->searchInput . trim('');
607 - $type = $request->type;
898 + $per_page = $request->getSafe('per_page', 'intval', 10);
899 + $search_input = $request->getSafe('searchInput', 'sanitize_text_field', '');
900 + $type = $request->getSafe('type', 'sanitize_text_field', 'to-do');
608 901
609 902 $currentUserId = get_current_user_id();
610 903
611 904 if (PermissionManager::isAdmin($currentUserId)) {
@@ -647,10 +940,13 @@
647 940 * @return
648 941 */
649 942 public function changeStageView($board_id, $stage_id)
650 943 {
944 + $board_id = absint($board_id);
945 + $stage_id = absint($stage_id);
946 +
651 947 try {
652 - $stage = Stage::findOrFail($stage_id);
948 + $stage = $this->findStageOnBoard($stage_id, $board_id);
653 949 $message = __('The stage is made public!', 'fluent-boards');
654 950 $settings = $stage->settings;
655 951
656 952 if (isset($settings['is_public'])) {
@@ -655,9 +951,9 @@
655 951
656 952 if (isset($settings['is_public'])) {
657 953 if ($settings['is_public']) {
658 954 $settings['is_public'] = false;
659 - $message = __('The stage is made admin only!', 'fluent-boards');
955 + $message = __('The stage is made private!', 'fluent-boards');
660 956 } else {
661 957 $settings['is_public'] = true;
662 958 }
663 959 } else {
@@ -676,24 +972,27 @@
676 972 }
677 973
678 974
679 975 /**
680 - * Set board background image or color
976 + * Set or reset board background image/color.
681 977 * @param \FluentBoards\Framework\Http\Request\Request $request
682 978 * @return
683 979 */
684 980 public function setBoardBackground(Request $request, $board_id)
685 981 {
686 - // sanitize and validate image_url
687 - if ($request->image_url) {
982 + $backgroundData = [];
983 + $isResetRequest = $request->getSafe('reset', 'rest_sanitize_boolean');
984 +
985 + if ($isResetRequest) {
986 + $backgroundData = [
987 + 'reset' => true,
988 + ];
989 + } elseif ($request->image_url) {
688 990 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
689 - "id" => 'required',
991 + 'id' => 'required|integer',
690 992 'image_url' => 'required|string|url',
691 993 ]);
692 - }
693 -
694 - // sanitize and validate color
695 - if ($request->color) {
994 + } elseif ($request->color) {
696 995 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
697 996 "id" => 'required',
698 997 'color' => 'required',
699 998 ]);
@@ -701,17 +1000,22 @@
701 1000
702 1001 try {
703 1002 if (!$board_id) {
704 1003 $errorMessage = __('Board id is required', 'fluent-boards');
705 - throw new \Exception($errorMessage, 400);
1004 + throw new \Exception(esc_html($errorMessage), 400);
706 1005 }
707 1006
1007 + if (empty($backgroundData)) {
1008 + $errorMessage = __('Background data is required', 'fluent-boards');
1009 + throw new \Exception(esc_html($errorMessage), 400);
1010 + }
1011 +
708 1012 return $this->sendSuccess([
709 1013 'message' => __('Background updated successfully', 'fluent-boards'),
710 1014 'background' => $this->boardService->setBoardBackground($backgroundData, $board_id),
711 1015 ]);
712 1016 } catch (\Exception $e) {
713 - $this->sendError([$e->getMessage(), 400]);
1017 + return $this->sendError($e->getMessage(), 400);
714 1018 }
715 1019 }
716 1020
717 1021
@@ -721,15 +1025,19 @@
721 1025 * @param mixed $stage_slug
722 1026 * @return $availablePositions as an array
723 1027 * @throws \Exception
724 1028 */
725 - public function getStageTaskAvailablePositions($board_id, $stage_id)
1029 + public function getStageTaskAvailablePositions(Request $request, $board_id, $stage_id)
726 1030 {
727 1031 try {
728 1032 if ($board_id && $stage_id) {
729 - $availablePositions = $this->boardService->getStageTaskAvailablePositions($board_id, $stage_id);
1033 + $taskId = $request->getSafe('task_id', 'intval');
1034 + $availablePositions = $this->boardService->getStageTaskAvailablePositions($board_id, $stage_id, $taskId);
730 1035 return $this->sendSuccess([
731 - 'availablePositions' => $availablePositions
1036 + 'availablePositions' => $availablePositions['availablePositions'],
1037 + 'moveTargets' => $availablePositions['moveTargets'],
1038 + 'currentMoveTargetKey' => $availablePositions['currentMoveTargetKey'],
1039 + 'defaultMoveTargetKey' => $availablePositions['defaultMoveTargetKey'],
732 1040 ], 200);
733 1041 } else {
734 1042 $message = '';
735 1043 if (!$board_id) {
@@ -737,12 +1045,12 @@
737 1045 }
738 1046 if (!$stage_id) {
739 1047 $message = 'Stage ';
740 1048 }
741 - throw new \Exception($message . 'is required', 400);
1049 + throw new \Exception(esc_html($message . 'is required'), 400);
742 1050 }
743 1051 } catch (\Exception $e) {
744 - $this->sendError([$e->getMessage(), 400]);
1052 + return $this->sendError($e->getMessage(), 400);
745 1053 }
746 1054 }
747 1055
748 1056 public function getAssociateCrmContacts($board_id)
@@ -751,24 +1059,47 @@
751 1059 $contactAssociatedTasks = Task::with('board')->where('board_id', $board_id)
752 1060 ->whereNotNull('crm_contact_id')
753 1061 ->get();
754 1062
755 - $formattedContacts = Collection::make($contactAssociatedTasks)
756 - ->groupBy('crm_contact_id')
757 - ->map(function ($tasks, $contactId) {
758 - $subscriber = Subscriber::find($contactId);
759 - if (!$subscriber) {
1063 + $tasksByContact = [];
1064 + foreach ($contactAssociatedTasks as $task) {
1065 + $tasksByContact[absint($task->crm_contact_id)][] = $task;
1066 + }
1067 +
1068 + $boardContactIds = Meta::query()
1069 + ->where('object_id', absint($board_id))
1070 + ->where('object_type', Constant::OBJECT_TYPE_BOARD)
1071 + ->whereIn('key', [
1072 + Constant::BOARD_ASSOCIATED_CRM_CONTACT,
1073 + self::LEGACY_BOARD_ASSOCIATED_CRM_CONTACT,
1074 + ])
1075 + ->pluck('value')
1076 + ->toArray();
1077 + $boardContactIds = array_values(array_unique(array_filter(array_map('absint', $boardContactIds))));
1078 +
1079 + $contactIds = array_values(array_unique(array_filter(array_map('absint', array_merge(
1080 + array_keys($tasksByContact),
1081 + $boardContactIds
1082 + )))));
1083 +
1084 + usort($contactIds, function ($firstContactId, $secondContactId) use ($boardContactIds) {
1085 + return (int) in_array($secondContactId, $boardContactIds, true) - (int) in_array($firstContactId, $boardContactIds, true);
1086 + });
1087 +
1088 + $formattedContacts = Collection::make($contactIds)
1089 + ->map(function ($contactId) use ($tasksByContact, $boardContactIds) {
1090 + $contact = Helper::crm_contact($contactId);
1091 + if (!$contact) {
760 1092 return null; // Skip if subscriber not found
761 1093 }
762 1094
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 - ];
1095 + $tasks = $tasksByContact[$contactId] ?? [];
1096 + $contact['name'] = trim(($contact['first_name'] ?? '') . ' ' . ($contact['last_name'] ?? '')) ?: ($contact['full_name'] ?? $contact['email'] ?? '');
1097 + $contact['crm_contact_id'] = $contactId;
1098 + $contact['is_board_contact'] = in_array($contactId, $boardContactIds, true);
1099 + $contact['tasks'] = $tasks;
1100 +
1101 + return $contact;
771 1102 })
772 1103 ->filter()->toArray();
773 1104
774 1105
@@ -789,11 +1120,13 @@
789 1120 'message' => __('Associated Crm Member has been updated', 'fluent-boards'),
790 1121 ], 200);
791 1122 }
792 1123
793 - public function hasDataChanged($board_id)
1124 + public function hasDataChanged(Request $request, $board_id)
794 1125 {
795 - return $this->boardService->hasDataChanged($board_id);
1126 + $includeArchived = filter_var($request->get('include_archived', false), FILTER_VALIDATE_BOOLEAN);
1127 + $since = $request->getSafe('since', 'sanitize_text_field');
1128 + return $this->boardService->hasDataChanged($board_id, $includeArchived, $since);
796 1129 }
797 1130
798 1131 public function createStage(Request $request, $board_id)
799 1132 {
@@ -816,15 +1149,27 @@
816 1149 }
817 1150
818 1151 public function moveAllTasks(Request $request, $board_id)
819 1152 {
820 - $oldStageId = $request->getSafe('oldStageId');
821 - $newStageId = $request->getSafe('newStageId');
1153 + $oldStageId = $request->getSafe('oldStageId', 'intval');
1154 + $newStageId = $request->getSafe('newStageId', 'intval');
822 1155
1156 + if (!$oldStageId || !$newStageId) {
1157 + return $this->sendError(__('Invalid stage IDs provided', 'fluent-boards'), 400);
1158 + }
1159 +
1160 + // Verify stages exist and belong to the board
1161 + $oldStage = Stage::where('id', $oldStageId)->where('board_id', $board_id)->first();
1162 + $newStage = Stage::where('id', $newStageId)->where('board_id', $board_id)->first();
1163 +
1164 + if (!$oldStage || !$newStage) {
1165 + return $this->sendError(__('One or both stages do not exist or do not belong to this board', 'fluent-boards'), 400);
1166 + }
1167 +
823 1168 $updates = $this->stageService->moveAllTasks($oldStageId, $newStageId, $board_id);
824 1169
825 1170 return [
826 - 'message' => __('Tasks has been Moved', 'fluent-boards'),
1171 + 'message' => __('Tasks have been moved', 'fluent-boards'),
827 1172 'updatedTasks' => $updates,
828 1173 ];
829 1174
830 1175 }
@@ -830,35 +1175,70 @@
830 1175 }
831 1176
832 1177 public function archiveAllTasksInStage($board_id, $stage_id)
833 1178 {
834 - $updates = $this->stageService->archiveAllTasksInStage($stage_id);
835 - return [
836 - 'message' => __('Tasks has been archived', 'fluent-boards'),
837 - 'updatedTasks' => $updates,
838 - ];
1179 + $board_id = absint($board_id);
1180 + $stage_id = absint($stage_id);
1181 +
1182 + try {
1183 + $this->findStageOnBoard($stage_id, $board_id);
1184 + $updates = $this->stageService->archiveAllTasksInStage($stage_id, $board_id);
1185 +
1186 + return [
1187 + 'message' => __('Tasks have been archived', 'fluent-boards'),
1188 + 'updatedTasks' => $updates,
1189 + ];
1190 + } catch (\Exception $e) {
1191 + return $this->sendError($e->getMessage(), 400);
1192 + }
839 1193 }
840 1194
841 1195 public function getAssociatedBoards(Request $request, $associated_id)
842 1196 {
843 - $associatedBoards = $this->boardService->getAssociatedBoards($associated_id);
1197 + if (!$this->currentUserCanReadCrmContacts()) {
1198 + return $this->sendError(esc_html__('You do not have permission to view CRM contact boards', 'fluent-boards'), 403);
1199 + }
1200 +
1201 + $associatedId = absint($associated_id);
1202 +
1203 + if (!$associatedId) {
1204 + return $this->sendError(__('Invalid CRM contact', 'fluent-boards'), 400);
1205 + }
1206 +
1207 + $associatedBoards = $this->boardService->getAssociatedBoards($associatedId, get_current_user_id());
1208 +
844 1209 return [
845 1210 'boards' => $associatedBoards,
846 1211 ];
847 1212 }
848 1213
1214 + private function currentUserCanReadCrmContacts()
1215 + {
1216 + $permissionManager = 'FluentCrm\\App\\Services\\PermissionManager';
1217 +
1218 + if (!class_exists($permissionManager)) {
1219 + return false;
1220 + }
1221 +
1222 + return (bool) $permissionManager::currentUserCan('fcrm_read_contacts');
1223 + }
1224 +
849 1225 public function duplicateBoard(Request $request, $board_id)
850 1226 {
851 1227 $boardData = $this->taskSanitizeAndValidate($request->get('board'), [
852 1228 'title' => 'required|string'
853 1229 ]);
1230 +
1231 + $boardData['source_board_id'] = $board_id;
1232 +
854 1233 $isWithLabels = $request->getSafe('isWithLabels');
855 1234 $isWithTasks = $request->getSafe('isWithTasks');
1235 + $isWithTemplates = $request->getSafe('isWithTemplates');
856 1236
857 1237 try {
858 1238 if(!PermissionManager::isAdmin()) {
859 1239 $errorMessage = __('You do not have permission to duplicate board', 'fluent-boards');
860 - throw new \Exception($errorMessage, 400);
1240 + throw new \Exception(esc_html($errorMessage), 400);
861 1241 }
862 1242 //create board
863 1243 $newBoard = $this->boardService->copyBoard($boardData);
864 1244
@@ -869,13 +1249,13 @@
869 1249 $labelMap = $this->labelService->copyLabelsOfBoard($board_id, $newBoard);
870 1250 }
871 1251
872 1252 //stage copy
873 - $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id);
1253 + $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id, $isWithTemplates);
874 1254
875 1255 //copy tasks of selected stages
876 1256 if ($isWithTasks == 'yes') {
877 - $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap);
1257 + $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap,$isWithTemplates);
878 1258 }
879 1259
880 1260 return $this->sendSuccess([
881 1261 'board' => $newBoard,
@@ -887,10 +1267,16 @@
887 1267
888 1268 public function importFromBoard(Request $request, $board_id)
889 1269 {
890 1270 $selectedStages = $request->getSafe('selectedStages');
891 - $position = $request->getSafe('position');
1271 + $position = $request->getSafe('position', 'intval');
892 1272
1273 + // Validate and sanitize selectedStages array
1274 + if (!is_array($selectedStages)) {
1275 + $selectedStages = [$selectedStages];
1276 + }
1277 + $selectedStages = array_filter(array_map('intval', $selectedStages));
1278 +
893 1279 try {
894 1280 $this->stageService->importStagesFromBoard($board_id, $selectedStages, $position);
895 1281
896 1282 return $this->sendSuccess([
@@ -965,7 +1351,184 @@
965 1351 ? 'manager'
966 1352 : ($boardRelation && Arr::has($boardRelation->settings, 'is_viewer_only') && Arr::get($boardRelation->settings, 'is_viewer_only')
967 1353 ? 'viewer'
968 1354 : 'member');
1355 + }
1356 + public function uploadBoardBackground(Request $request,$board_id)
1357 + {
1358 + $file = Arr::get($request->files(), 'file')->toArray();
1359 + (new \FluentBoards\App\Services\UploadService)->validateFile($file);
1360 +
1361 + $uploadInfo = UploadService::handleFileUpload( $request->files(), $board_id);
1362 +
1363 + $fileData = $uploadInfo[0];
1364 + $initialDataData = [
1365 + 'type' => 'url',
1366 + 'url' => '',
1367 + 'name' => '',
1368 + 'size' => 0,
1369 + ];
1370 +
1371 + $attachData = array_merge($initialDataData, $fileData);
1372 + $UrlMeta = [];
1373 + if($attachData['type'] == 'url') {
1374 + $UrlMeta = RemoteUrlParser::parse($attachData['url']);
1375 + }
1376 + $uid = wp_generate_uuid4();
1377 + $fileUploadedData = new Attachment();
1378 + $fileUploadedData->object_id = $board_id;
1379 + $fileUploadedData->object_type = Constant::BOARD_BACKGROUND_IMAGE;
1380 + $fileUploadedData->attachment_type = $attachData['type'];
1381 + $fileUploadedData->title = (new TaskService())->setTitle($attachData['type'], $attachData['name'], $UrlMeta);
1382 + $fileUploadedData->file_path = $attachData['type'] != 'url' ? $attachData['file'] : null;
1383 + $fileUploadedData->full_url = esc_url($attachData['url']);
1384 + $fileUploadedData->file_size = $attachData['size'];
1385 + $fileUploadedData->settings = $attachData['type'] == 'url' ? [
1386 + 'meta' => $UrlMeta
1387 + ] : '';
1388 + $fileUploadedData->driver = 'local';
1389 + $fileUploadedData->file_hash = md5($uid . wp_rand(0, 1000));
1390 + $fileUploadedData->save();
1391 + if(!!defined('FLUENT_BOARDS_PRO_VERSION')) {
1392 + $mediaData = (new AttachmentService())->processMediaData($fileData, $file);
1393 + $fileUploadedData['driver'] = $mediaData['driver'];
1394 + $fileUploadedData['file_path'] = $mediaData['file_path'];
1395 + $fileUploadedData['full_url'] = $mediaData['full_url'];
1396 + $fileUploadedData->save();
1397 + }
1398 +
1399 + $board = Board::find($board_id);
1400 + $oldBackground = $board->background;
1401 + $publicUrl = (new CommentService())->createPublicUrl($fileUploadedData, $board_id);
1402 + $background = [
1403 + 'color' => null,
1404 + 'id' => $fileUploadedData->id,
1405 + 'image_url' => $publicUrl,
1406 + 'is_image' => true,
1407 + ];
1408 + $board->background = $background;
1409 + $board->save();
1410 + do_action('fluent_boards/board_background_updated', $board_id, $oldBackground);
1411 +
1412 + return $this->sendSuccess([
1413 + 'message' => __('Background updated successfully', 'fluent-boards'),
1414 + 'background' => $board->background,
1415 + ]);
1416 + }
1417 +
1418 + public function getPinnedBoards()
1419 + {
1420 + $pinnedBoards = $this->boardService->getPinnedBoards();
1421 +
1422 + return $this->sendSuccess([
1423 + 'pinnedBoards' => $pinnedBoards,
1424 + ], 200);
1425 + }
1426 +
1427 + public function pinBoard($boardId)
1428 + {
1429 + $this->boardService->pinBoard($boardId);
1430 +
1431 + return $this->sendSuccess([
1432 + 'message' => __('The Board has been pinned', 'fluent-boards'),
1433 + ], 200);
1434 + }
1435 +
1436 + public function unpinBoard($boardId)
1437 + {
1438 + $remove = $this->boardService->unpinBoard($boardId);
1439 +
1440 + if (!$remove) {
1441 + return $this->sendError([
1442 + 'message' => __('Board is not pinned', 'fluent-boards'),
1443 + ], 400);
1444 + }
1445 +
1446 + return $this->sendSuccess([
1447 + 'message' => __('Board is removed from pinned boards', 'fluent-boards'),
1448 + ], 200);
1449 + }
1450 +
1451 + public function getBoardFolder($board_id)
1452 + {
1453 + try {
1454 + $folder = $this->boardService->getBoardFolder($board_id);
1455 + return $this->sendSuccess([
1456 + 'folder' => $folder,
1457 + ], 200);
1458 + } catch (\Exception $e) {
1459 + return $this->sendError($e->getMessage(), 400);
1460 + }
1461 + }
1462 +
1463 + public function getBoardMenuItems($board_id)
1464 + {
1465 + try {
1466 + $menuItems = (new BoardMenuHandler())->getMenuItems($board_id);
1467 +
1468 + return $this->sendSuccess([
1469 + 'menu_items' => $menuItems
1470 + ], 200);
1471 + } catch (\Exception $e) {
1472 + return $this->sendError($e->getMessage(), 500);
1473 + }
1474 + }
1475 +
1476 + public function getPublicAccessSettings($board_id)
1477 + {
1478 + $board_id = absint($board_id);
1479 + $board = Board::findOrFail($board_id);
1480 +
1481 + $enabled = (bool) $board->getMetaByKey('public_access_enabled');
1482 + $shortcode = $enabled ? '[fluent_board_public id="' . $board_id . '"]' : '';
1483 +
1484 + return $this->sendSuccess([
1485 + 'enabled' => $enabled,
1486 + 'shortcode' => $shortcode,
1487 + ], 200);
1488 + }
1489 +
1490 + public function togglePublicAccess(Request $request, $board_id)
1491 + {
1492 + $board_id = absint($board_id);
1493 + $board = Board::findOrFail($board_id);
1494 +
1495 + $enabled = filter_var(
1496 + $request->getSafe('enabled', 'sanitize_text_field', false),
1497 + FILTER_VALIDATE_BOOLEAN
1498 + );
1499 +
1500 + $board->updateMeta('public_access_enabled', $enabled ? '1' : '');
1501 +
1502 + $shortcode = $enabled ? '[fluent_board_public id="' . $board_id . '"]' : '';
1503 +
1504 + return $this->sendSuccess([
1505 + 'message' => $enabled
1506 + ? __('Public access has been enabled', 'fluent-boards')
1507 + : __('Public access has been disabled', 'fluent-boards'),
1508 + 'enabled' => $enabled,
1509 + 'shortcode' => $shortcode,
1510 + ], 200);
1511 + }
1512 +
1513 + /**
1514 + * Resolve a stage only when it belongs to the requested board.
1515 + *
1516 + * @param int $stageId
1517 + * @param int $boardId
1518 + * @return Stage
1519 + * @throws \Exception
1520 + */
1521 + private function findStageOnBoard($stageId, $boardId)
1522 + {
1523 + $stage = Stage::where('id', absint($stageId))
1524 + ->where('board_id', absint($boardId))
1525 + ->first();
1526 +
1527 + if (!$stage) {
1528 + throw new \Exception(esc_html__('Stage not found', 'fluent-boards'));
1529 + }
1530 +
1531 + return $stage;
969 1532 }
970 1533
971 1534 }