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 +746 -135 1.21trunk 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');
@@ -53,15 +64,48 @@
53 64 $order = $request->getSafe('order', 'sanitize_text_field', 'created_at');
54 65 $orderBy = $request->getSafe('orderBy', 'sanitize_text_field', 'DESC');
55 66 $searchInput = $request->getSafe('searchInput', 'sanitize_text_field');
56 67
57 - if(!defined('FLUENT_ROADMAP'))
58 - {
59 - $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
68 + $option = $request->getSafe('option', 'sanitize_text_field');
69 + $folderId = $request->getSafe('fid', 'intval'); // Get folder ID from request
70 +
71 + // Initialize the query based on archive status
72 + if (!defined('FLUENT_ROADMAP')) {
73 + if ($option == 'archived') {
74 + $relatedBoardsQuery = Board::whereNotNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
75 + } else {
76 + $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
77 + }
60 78 } else {
61 - $relatedBoardsQuery = Board::whereNull('archived_at')->byAccessUser($userId);
79 + if ($option == 'archived') {
80 + $relatedBoardsQuery = Board::whereNotNull('archived_at')->byAccessUser($userId);
81 + } else {
82 + $relatedBoardsQuery = Board::whereNull('archived_at')->byAccessUser($userId);
83 + }
62 84 }
63 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
64 108 if (!empty($searchInput)) {
65 109 $relatedBoardsQuery = $relatedBoardsQuery->where('title', 'like', '%' . $searchInput . '%');
66 110 }
67 111
@@ -70,37 +114,114 @@
70 114 ->with('stages', 'users')
71 115 ->paginate($per_page);
72 116
73 117 foreach ($relatedBoards as $relatedBoard) {
118 + $relatedBoard->description = DescriptionMarkdownConverter::normalize($relatedBoard->description);
74 119 $relatedBoard->users = Helper::sanitizeUserCollections($relatedBoard->users);
120 + $relatedBoard->is_pinned = $this->boardService->isPinned($relatedBoard->id);
75 121 }
76 122
123 + $response = [
124 + 'boards' => $relatedBoards,
125 + 'board_counts' => $this->boardService->getBoardCounts($userId)
126 + ];
127 +
128 + $folderMapping = $this->getBoardFolderMapping($userId);
129 + $response['folder_mapping'] = $folderMapping;
130 + if ($folderId) {
131 + $response['current_folder'] = isset($folderMapping[$folderId])
132 + ? $this->getCurrentFolderInfoFromMapping($folderMapping[$folderId])
133 + : null;
134 + }
135 +
136 + return $this->sendSuccess($response);
137 + }
138 +
139 + /**
140 + * Get folder mapping for boards
141 + */
142 + private function getBoardFolderMapping($userId)
143 + {
144 + $folderService = new FolderService();
145 + $folders = $folderService->getFolders($userId);
146 +
147 + $mapping = [];
148 + foreach ($folders as $folder) {
149 + $mapping[$folder->id] = [
150 + 'id' => $folder->id,
151 + 'title' => $folder->title,
152 + 'board_ids' => $folder->boards ? $folder->boards->pluck('id')->toArray() : []
153 + ];
154 + }
155 +
156 + return $mapping;
157 + }
158 +
159 + private function getCurrentFolderInfoFromMapping(array $folder)
160 + {
161 + return [
162 + 'id' => $folder['id'],
163 + 'title' => $folder['title'],
164 + 'board_count' => count($folder['board_ids'])
165 + ];
166 + }
167 +
168 + /**
169 + * Get the list of boards and their associated stages for the current user.
170 + *
171 + * @param \FluentBoards\Framework\Http\Request\Request $request
172 + * @return \WP_REST_Response
173 + */
174 + public function getBoardsList(Request $request)
175 + {
176 + $userId = get_current_user_id();
177 +
178 + // Query to fetch boards that are not archived and accessible by the user
179 + // Check if the FLUENT_ROADMAP constant is defined
180 + if (!defined('FLUENT_ROADMAP')) {
181 + $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->excludeTemplates()->byAccessUser($userId);
182 + } else {
183 + $relatedBoardsQuery = Board::whereNull('archived_at')->excludeTemplates()->byAccessUser($userId);
184 + }
185 +
186 + $relatedBoards = $relatedBoardsQuery->with('stages')->get();
187 +
188 + // Fetch the stages associated with the boards
189 + $stages = Stage::whereIn('board_id', $relatedBoards->pluck('id'))->where('archived_at', null)->get();
190 +
77 191 return $this->sendSuccess([
78 - 'boards' => $relatedBoards
192 + 'boards' => $relatedBoards,
193 + 'all_stages' => $stages,
79 194 ], 200);
80 195 }
81 -
82 - public function getBoardsList(Request $request)
196 + public function getOnlyBoardsByUser(Request $request)
83 197 {
84 198 try {
85 199 $userId = get_current_user_id();
86 200
87 - if (PermissionManager::isAdmin($userId)) {
88 - $relatedBoardsQuery = Board::query()->where('type', 'to-do');
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);
89 207 } else {
90 - $currentUser = User::find($userId);
91 - $relatedBoardsQuery = $currentUser->whichBoards()->where('type', 'to-do');
208 + $relatedBoardsQuery = Board::whereNull('archived_at')->excludeTemplates()->byAccessUser($userId);
92 209 }
93 210
94 - $relatedBoards = $relatedBoardsQuery->with('stages')->get();
95 - $stages = Stage::whereIn('board_id', $relatedBoards->pluck('id'))->get();
211 + if (!empty($searchInput)) {
212 + $relatedBoardsQuery = $relatedBoardsQuery->where('title', 'like', '%' . $searchInput . '%');
213 + }
96 214
215 + $relatedBoards = $relatedBoardsQuery->orderBy('created_at', 'DESC')->get();
216 +
97 217 return $this->sendSuccess([
98 - 'boards' => $relatedBoards,
99 - 'all_stages' => $stages,
100 - ], 200);
218 + 'boards' => $relatedBoards
219 + ]);
101 220 } catch (\Exception $e) {
102 - return $this->sendError($e->getMessage(), 404);
221 + return $this->sendError([
222 + 'message' => $e->getMessage()
223 + ]);
103 224 }
104 225 }
105 226
106 227 public function getRecentBoards()
@@ -107,9 +228,12 @@
107 228 {
108 229 $boards = $this->boardService->getRecentBoards();
109 230
110 231 if (!$boards || $boards->isEmpty()) {
111 - $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())
112 236 ->limit(4)
113 237 ->withCount('completedTasks')
114 238 ->with(['stages', 'users'])
115 239 ->get();
@@ -116,8 +240,9 @@
116 240 }
117 241
118 242 foreach ($boards as $board) {
119 243 $board->users = Helper::sanitizeUserCollections($board->users);
244 + $board->is_pinned = $this->boardService->isPinned($board->id);
120 245 }
121 246
122 247 return [
123 248 'boards' => $boards,
@@ -132,9 +257,9 @@
132 257 $boards = $this->boardService->getBoardsByType($type);
133 258
134 259 return $this->sendSuccess([
135 260 'boards' => $boards,
136 - ], 200);
261 + ]);
137 262 }
138 263
139 264 public function createFirstBoard(Request $request)
140 265 {
@@ -145,11 +270,14 @@
145 270 'currency' => 'nullable|string',
146 271 'crm_contact_id' => 'nullable|numeric',
147 272 ]);
148 273
149 - $installFluentCRM = $request->get('withFluentCRM') == 'yes' ? true : false;
274 + $installFluentCRM = $request->getSafe('withFluentCRM', 'sanitize_text_field') == 'yes' ? true : false;
150 275
151 276 $postStages = $request->get('stages');
277 + if (!is_array($postStages)) {
278 + $postStages = [];
279 + }
152 280 $stageData = array();
153 281 foreach ($postStages as $stage) {
154 282 $temp = $this->stageSanitizeAndValidate($stage, [
155 283 'title' => 'required|string',
@@ -159,9 +287,9 @@
159 287
160 288 $taskData = null;
161 289 if ($request->get('task')) {
162 290 $taskData = $this->taskSanitizeAndValidate($request->get('task'), [
163 - 'title' => 'required|string'
291 + 'title' => 'required|string',
164 292 ]);
165 293 }
166 294
167 295 $board = $this->boardService->createBoard($boardData);
@@ -175,9 +303,9 @@
175 303 $this->taskService->createTask($taskData, $board->id);
176 304 }
177 305
178 306 do_action('fluent_boards/board_created', $board);
179 -
307 +
180 308 if ($installFluentCRM && !defined('FLUENTCRM')) {
181 309 InstallService::install('fluent-crm');
182 310 }
183 311
@@ -186,8 +314,21 @@
186 314 'board' => $board,
187 315 ];
188 316 }
189 317
318 + public function skipOnboarding(Request $request)
319 + {
320 + $onboarding = Meta::where('key', Constant::FBS_ONBOARDING)->first();
321 + if($onboarding && $onboarding->value == 'no'){
322 + $onboarding->value = 'yes' ;
323 + $onboarding->save();
324 + }
325 +
326 + return [
327 + 'message' => __('Onboarding skipped successfully', 'fluent-boards'),
328 + ];
329 + }
330 +
190 331 public function create(Request $request)
191 332 {
192 333 $boardData = $this->boardSanitizeAndValidate($request->get('board'), [
193 334 'title' => 'required|string',
@@ -194,17 +335,44 @@
194 335 'description' => 'nullable',
195 336 'type' => 'required|string',
196 337 'currency' => 'nullable|string',
197 338 'crm_contact_id' => 'nullable|numeric',
339 + 'folder_id' => 'nullable',
198 340 ]);
199 341
200 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 +
201 354 $board = $this->boardService->createBoard($boardData);
202 - $this->labelService->createDefaultLabel($board->id);
355 + $this->createBoardLabelsFromRequest($request, $board->id);
356 + $this->addBoardMembersFromRequest($request, $board->id);
203 357 $type = ucfirst($boardData['type']);
358 + $stages = $request->get('stages');
359 + $sanitizedStages = [];
204 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 +
205 371 if (isset($boardData['type']) && $boardData['type'] == 'roadmap') {
206 - $this->stageService->createRoadmapStages($board, $request->get('stages'));
372 + $this->stageService->createRoadmapStages($board, $sanitizedStages);
373 + } elseif (!empty($sanitizedStages)) {
374 + $this->stageService->createStages($board, $sanitizedStages);
207 375 } else {
208 376 $this->stageService->createDefaultStages($board);
209 377 }
210 378
@@ -214,37 +382,118 @@
214 382 }
215 383
216 384 do_action('fluent_boards/board_created', $board);
217 385
386 +
387 + if ($folderId) {
388 + $folderService->addBoardToFolder($folderId, [$board->id]);
389 + }
390 +
218 391 $message = __('Board has been created successfully', 'fluent-boards');
219 392
220 - return $this->sendSuccess([
393 + return $this->send([
221 394 'message' => $message,
222 395 'board' => $board,
223 396 ], 201);
224 397 } catch (\Exception $e) {
225 - return $this->sendError($e->getMessage(), 400);
398 + return $this->sendError([
399 + 'message' => $e->getMessage()
400 + ]);
226 401 }
227 402 }
228 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 + */
229 484 public function getArchivedStage(Request $request, $board_id)
230 485 {
231 486 try {
232 - $pagination = $request->noPagination ? true : false;
233 - $per_page = isset($data['per_page']) ? $data['per_page'] : 30;
234 - $page = isset($data['page']) ? $data['page'] : 1;
235 - if ($pagination) {
236 - $stages = Stage::where('board_id', $board_id)
237 - ->whereNotNull('archived_at')
238 - ->orderBy('created_at', 'DESC')
239 - ->get();
240 - } else {
241 - $stages = Stage::where('board_id', $board_id)
242 - ->whereNotNull('archived_at')
243 - ->orderBy('created_at', 'DESC')
244 - ->paginate($per_page, ['*'], 'page', $page);
245 - }
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 + ];
246 493
494 + $stages = $this->stageService->getArchivedStages($sanitizedParams, $board_id);
495 +
247 496 return $this->sendSuccess([
248 497 'stages' => $stages,
249 498 ], 200);
250 499 } catch (\Exception $e) {
@@ -251,17 +500,33 @@
251 500 return $this->sendError($e->getMessage(), 404);
252 501 }
253 502 }
254 503
255 - public function find($board_id)
504 + public function find(Request $request, $board_id)
256 505 {
257 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);
258 509 $board->background = maybe_unserialize($board->background);
259 510 $board->createdOn = $board->created_at->format('Y-m-d');
260 511
261 - $board->load(['users', 'stages', 'labels', 'owner']);
512 + $board->load(['users', 'labels', 'owner']);
262 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 +
263 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 +
264 529 $board->load(['customFields']);
265 530 }
266 531
267 532 $this->boardService->updateRecentBoards($board_id);
@@ -271,17 +536,30 @@
271 536
272 537 $board->users = Helper::sanitizeUserCollections($board->users);
273 538 $board->owner = Helper::sanitizeUserCollections($board->owner);
274 539
540 + $board->is_pinned = $this->boardService->isPinned($board->id);
541 +
275 542 $board = apply_filters('fluent_boards/board_find', $board);
276 543
277 544 return [
278 - 'board' => $board
545 + 'board' => $board,
546 + 'synced_at' => current_time('mysql')
279 547 ];
280 548 }
281 549
282 550 public function update(Request $request, $board_id)
283 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 +
284 562 $boardData = $this->boardSanitizeAndValidate($request->only(['title', 'description']), [
285 563 'title' => 'required|string',
286 564 'description' => 'nullable|string',
287 565 ]);
@@ -286,8 +564,9 @@
286 564 'description' => 'nullable|string',
287 565 ]);
288 566
289 567 $board = Board::findOrFail($board_id);
568 + $boardData['description'] = DescriptionMarkdownConverter::normalize($boardData['description']);
290 569
291 570 $oldBoard = clone $board;
292 571 $board->fill($boardData);
293 572 $board->save();
@@ -294,21 +573,23 @@
294 573
295 574 do_action('fluent_boards/board_updated', $board, $oldBoard);
296 575
297 576 return [
298 - 'stages' => $board->stages()->get(),
299 577 'message' => __('Board has been updated', 'fluent-boards'),
300 578 'board' => $board,
579 + 'stages' => $board->stages()->get(),
301 580 ];
302 581 }
303 582
304 583 public function archiveStage($board_id, $stage_id)
305 584 {
585 + $board_id = absint($board_id);
586 + $stage_id = absint($stage_id);
587 +
306 588 try {
307 - $stage = Stage::findOrFail($stage_id);
308 - $board = Board::findOrFail($stage->board_id);
589 + $stage = $this->findStageOnBoard($stage_id, $board_id);
309 590
310 - $updatedStage = $this->boardService->archiveStage($board->id, $stage);
591 + $updatedStage = $this->boardService->archiveStage($board_id, $stage);
311 592
312 593 return $this->sendSuccess([
313 594 'updatedStage' => $updatedStage,
314 595 'message' => __('Stage has been archived', 'fluent-boards'),
@@ -319,18 +600,20 @@
319 600 }
320 601
321 602 public function restoreStage($board_id, $stage_id)
322 603 {
604 + $board_id = absint($board_id);
605 + $stage_id = absint($stage_id);
606 +
323 607 try {
324 - $stage = Stage::findOrFail($stage_id);
325 - $board = Board::findOrFail($board_id);
608 + $stage = $this->findStageOnBoard($stage_id, $board_id);
326 609
327 - $updatedStage = $this->boardService->restoreStage($board->id, $stage);
610 + $updatedStage = $this->boardService->restoreStage($board_id, $stage);
328 611
329 612 return $this->sendSuccess([
330 - 'success' => true,
613 + 'success' => true,
331 614 'updatedStage' => $updatedStage,
332 - 'message' => __('Stage has been restored', 'fluent-boards')
615 + 'message' => __('Stage has been restored', 'fluent-boards')
333 616 ], 200);
334 617 } catch (\Exception $e) {
335 618 return $this->sendError($e->getMessage(), 400);
336 619 }
@@ -336,32 +619,22 @@
336 619 }
337 620 }
338 621
339 622
340 - public function changePositionOfStage(Request $request, $board_id)
623 + public function repositionStages(Request $request, $board_id)
341 624 {
342 - $changeData = $this->boardSanitizeAndValidate($request->only(['fromPosition', 'toPosition']), [
343 - 'fromPosition' => 'required',
344 - 'toPosition' => 'required',
345 - ]);
346 -
625 + $incomingList = $request->get('list');
626 + if (!is_array($incomingList)) {
627 + $incomingList = [];
628 + }
629 + $incomingList = array_map('intval', $incomingList);
347 630 try {
348 - $this->boardService->changePositionOfStage($board_id, $changeData);
631 + foreach ($incomingList as $stageId) {
632 + $this->findStageOnBoard($stageId, $board_id);
633 + }
349 634
635 + $this->boardService->repositionStages($board_id, $incomingList);
350 636 return $this->sendSuccess([
351 - 'message' => __('Board stage has been updated', 'fluent-boards')
352 - ], 200);
353 - } catch (\Exception $e) {
354 - return $this->sendError($e->getMessage(), 400);
355 - }
356 - }
357 -
358 - public function rePositionStages(Request $request, $board_id)
359 - {
360 - $incomingList = $request->get('list');
361 - try {
362 - $this->boardService->rePositionStages($board_id, $incomingList);
363 - return $this->sendSuccess([
364 637 'message' => __('Stages Reordered', 'fluent-boards'),
365 638 'updatedStages' => $this->stageService->getLastOneMinuteUpdatedStages($board_id)
366 639 ], 200);
367 640 } catch (\Exception $e) {
@@ -379,9 +652,9 @@
379 652 public function delete($board_id)
380 653 {
381 654 try {
382 655 if (!PermissionManager::isAdmin()) {
383 - 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);
384 657 }
385 658 $this->boardService->deleteBoard($board_id);
386 659
387 660 return $this->sendSuccess([
@@ -399,9 +672,12 @@
399 672
400 673 public function getActivities(Request $request, $board_id)
401 674 {
402 675 try {
403 - $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 + ]);
404 680 return $this->sendSuccess([
405 681 'activities' => $activities,
406 682 ], 200);
407 683 } catch (\Exception $e) {
@@ -419,8 +695,11 @@
419 695 $boardObjects = Relation::where('object_type', 'board_user')
420 696 ->where('object_id', $board_id)
421 697 ->get()->keyBy('foreign_id');
422 698
699 + $superAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
700 + ->get()->pluck('object_id')->toArray();
701 +
423 702 $userIds = $boardObjects->pluck('foreign_id')->toArray();
424 703
425 704 $coreUsers = [];
426 705 if ($userIds) {
@@ -439,14 +718,18 @@
439 718 }
440 719
441 720 $boardRelation = $boardObjects[$user->ID] ?? null;
442 721
722 +
443 723 $formattedUsers[] = [
444 724 'ID' => $user->ID,
445 725 'display_name' => $name,
726 + 'user_login' => $user->user_login,
446 727 'email' => $user->user_email,
447 728 'photo' => fluent_boards_user_avatar($user->user_email, $name),
448 - 'role' => $boardRelation && $boardRelation->settings['is_admin'] ? 'manager' : 'member'
729 + 'role' => $this->boardUserRole($boardRelation),
730 + 'is_super' => in_array($user->ID, $superAdminIds),
731 + 'is_wpadmin' => $user->has_cap('manage_options')
449 732 ];
450 733 }
451 734
452 735 // order formatted users by display_name
@@ -463,16 +746,24 @@
463 746 return $returnData;
464 747 }
465 748
466 749 /*
467 - * 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.
468 751 */
469 - $adminUserIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
752 + $fluentBoardAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
470 753 ->whereNotIn('object_id', $userIds)
471 754 ->get()
472 755 ->pluck('object_id')
473 756 ->toArray();
474 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 +
475 766 if ($adminUserIds) {
476 767 $adminUsers = get_users([
477 768 'include' => $adminUserIds,
478 769 ]);
@@ -489,9 +780,11 @@
489 780 'ID' => $user->ID,
490 781 'display_name' => $name,
491 782 'email' => $user->user_email,
492 783 'photo' => fluent_boards_user_avatar($user->user_email, $name),
493 - 'role' => 'admin'
784 + 'role' => 'admin',
785 + 'is_super' => in_array($user->ID, $superAdminIds),
786 + 'is_wpadmin' => $user->has_cap('manage_options')
494 787 ];
495 788 }
496 789
497 790 // order formatted users by display_name
@@ -520,18 +813,25 @@
520 813 }
521 814
522 815 public function addMembersInBoard(Request $request, $board_id)
523 816 {
524 - $memberId = $request->getSafe('memberId');
525 - $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);
526 820
527 - if ($isAlreadyMember) {
821 + if ($member === null) {
528 822 return $this->sendError([
823 + 'message' => __('User not found.', 'fluent-boards'),
824 + ], 404);
825 + }
826 +
827 + if (!$member) {
828 + return $this->sendError([
529 829 'message' => __('User already a member', 'fluent-boards'),
530 - ], 304);
830 + ], 409);
531 831 }
532 - $member = $this->boardService->addMembersInBoard($board_id, $memberId);
533 832
833 +
534 834 return [
535 835 'message' => __('Member added successfully', 'fluent-boards'),
536 836 'member' => Helper::sanitizeUserCollections($member)
537 837 ];
@@ -559,11 +859,11 @@
559 859 }
560 860
561 861 public function searchBoards(Request $request)
562 862 {
563 - $per_page = $request->get('per_page', 10);
564 - $search_input = $request->searchInput . trim('');
565 - $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');
566 866
567 867 $currentUserId = get_current_user_id();
568 868
569 869 if (PermissionManager::isAdmin($currentUserId)) {
@@ -605,10 +905,13 @@
605 905 * @return
606 906 */
607 907 public function changeStageView($board_id, $stage_id)
608 908 {
909 + $board_id = absint($board_id);
910 + $stage_id = absint($stage_id);
911 +
609 912 try {
610 - $stage = Stage::findOrFail($stage_id);
913 + $stage = $this->findStageOnBoard($stage_id, $board_id);
611 914 $message = __('The stage is made public!', 'fluent-boards');
612 915 $settings = $stage->settings;
613 916
614 917 if (isset($settings['is_public'])) {
@@ -613,9 +916,9 @@
613 916
614 917 if (isset($settings['is_public'])) {
615 918 if ($settings['is_public']) {
616 919 $settings['is_public'] = false;
617 - $message = __('The stage is made admin only!', 'fluent-boards');
920 + $message = __('The stage is made private!', 'fluent-boards');
618 921 } else {
619 922 $settings['is_public'] = true;
620 923 }
621 924 } else {
@@ -634,24 +937,27 @@
634 937 }
635 938
636 939
637 940 /**
638 - * Set board background image or color
941 + * Set or reset board background image/color.
639 942 * @param \FluentBoards\Framework\Http\Request\Request $request
640 943 * @return
641 944 */
642 945 public function setBoardBackground(Request $request, $board_id)
643 946 {
644 - // sanitize and validate image_url
645 - 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) {
646 955 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
647 - "id" => 'required',
956 + 'id' => 'required|integer',
648 957 'image_url' => 'required|string|url',
649 958 ]);
650 - }
651 -
652 - // sanitize and validate color
653 - if ($request->color) {
959 + } elseif ($request->color) {
654 960 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
655 961 "id" => 'required',
656 962 'color' => 'required',
657 963 ]);
@@ -659,17 +965,22 @@
659 965
660 966 try {
661 967 if (!$board_id) {
662 968 $errorMessage = __('Board id is required', 'fluent-boards');
663 - throw new \Exception($errorMessage, 400);
969 + throw new \Exception(esc_html($errorMessage), 400);
664 970 }
665 971
972 + if (empty($backgroundData)) {
973 + $errorMessage = __('Background data is required', 'fluent-boards');
974 + throw new \Exception(esc_html($errorMessage), 400);
975 + }
976 +
666 977 return $this->sendSuccess([
667 978 'message' => __('Background updated successfully', 'fluent-boards'),
668 979 'background' => $this->boardService->setBoardBackground($backgroundData, $board_id),
669 980 ]);
670 981 } catch (\Exception $e) {
671 - $this->sendError([$e->getMessage(), 400]);
982 + return $this->sendError($e->getMessage(), 400);
672 983 }
673 984 }
674 985
675 986
@@ -679,15 +990,19 @@
679 990 * @param mixed $stage_slug
680 991 * @return $availablePositions as an array
681 992 * @throws \Exception
682 993 */
683 - public function getStageTaskAvailablePositions($board_id, $stage_id)
994 + public function getStageTaskAvailablePositions(Request $request, $board_id, $stage_id)
684 995 {
685 996 try {
686 997 if ($board_id && $stage_id) {
687 - $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);
688 1000 return $this->sendSuccess([
689 - 'availablePositions' => $availablePositions
1001 + 'availablePositions' => $availablePositions['availablePositions'],
1002 + 'moveTargets' => $availablePositions['moveTargets'],
1003 + 'currentMoveTargetKey' => $availablePositions['currentMoveTargetKey'],
1004 + 'defaultMoveTargetKey' => $availablePositions['defaultMoveTargetKey'],
690 1005 ], 200);
691 1006 } else {
692 1007 $message = '';
693 1008 if (!$board_id) {
@@ -695,12 +1010,12 @@
695 1010 }
696 1011 if (!$stage_id) {
697 1012 $message = 'Stage ';
698 1013 }
699 - throw new \Exception($message . 'is required', 400);
1014 + throw new \Exception(esc_html($message . 'is required'), 400);
700 1015 }
701 1016 } catch (\Exception $e) {
702 - $this->sendError([$e->getMessage(), 400]);
1017 + return $this->sendError($e->getMessage(), 400);
703 1018 }
704 1019 }
705 1020
706 1021 public function getAssociateCrmContacts($board_id)
@@ -709,24 +1024,47 @@
709 1024 $contactAssociatedTasks = Task::with('board')->where('board_id', $board_id)
710 1025 ->whereNotNull('crm_contact_id')
711 1026 ->get();
712 1027
713 - $formattedContacts = Collection::make($contactAssociatedTasks)
714 - ->groupBy('crm_contact_id')
715 - ->map(function ($tasks, $contactId) {
716 - $subscriber = Subscriber::find($contactId);
717 - 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) {
718 1057 return null; // Skip if subscriber not found
719 1058 }
720 1059
721 - return [
722 - 'name' => $subscriber->first_name . ' ' . $subscriber->last_name,
723 - 'photo' => $subscriber->photo,
724 - 'email' => $subscriber->email,
725 - 'crm_contact_id' => $contactId,
726 - 'id' => $contactId,
727 - 'tasks' => $tasks,
728 - ];
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;
729 1067 })
730 1068 ->filter()->toArray();
731 1069
732 1070
@@ -747,11 +1085,13 @@
747 1085 'message' => __('Associated Crm Member has been updated', 'fluent-boards'),
748 1086 ], 200);
749 1087 }
750 1088
751 - public function hasDataChanged($board_id)
1089 + public function hasDataChanged(Request $request, $board_id)
752 1090 {
753 - 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);
754 1094 }
755 1095
756 1096 public function createStage(Request $request, $board_id)
757 1097 {
@@ -756,8 +1096,9 @@
756 1096 public function createStage(Request $request, $board_id)
757 1097 {
758 1098 $stageData = $this->stageSanitizeAndValidate($request->all(), [
759 1099 'title' => 'required|string',
1100 + 'position' => 'nullable|numeric'
760 1101 ]);
761 1102
762 1103 $board = Board::find($board_id);
763 1104 $stage = $this->stageService->createStage($stageData, $board_id);
@@ -773,15 +1114,27 @@
773 1114 }
774 1115
775 1116 public function moveAllTasks(Request $request, $board_id)
776 1117 {
777 - $oldStageId = $request->getSafe('oldStageId');
778 - $newStageId = $request->getSafe('newStageId');
1118 + $oldStageId = $request->getSafe('oldStageId', 'intval');
1119 + $newStageId = $request->getSafe('newStageId', 'intval');
779 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 +
780 1133 $updates = $this->stageService->moveAllTasks($oldStageId, $newStageId, $board_id);
781 1134
782 1135 return [
783 - 'message' => __('Tasks has been Moved', 'fluent-boards'),
1136 + 'message' => __('Tasks have been moved', 'fluent-boards'),
784 1137 'updatedTasks' => $updates,
785 1138 ];
786 1139
787 1140 }
@@ -787,50 +1140,87 @@
787 1140 }
788 1141
789 1142 public function archiveAllTasksInStage($board_id, $stage_id)
790 1143 {
791 - $updates = $this->stageService->archiveAllTasksInStage($stage_id);
792 - return [
793 - 'message' => __('Tasks has been archived', 'fluent-boards'),
794 - 'updatedTasks' => $updates,
795 - ];
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 + }
796 1158 }
797 1159
798 1160 public function getAssociatedBoards(Request $request, $associated_id)
799 1161 {
800 - $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 +
801 1174 return [
802 1175 'boards' => $associatedBoards,
803 1176 ];
804 1177 }
805 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 +
806 1190 public function duplicateBoard(Request $request, $board_id)
807 1191 {
808 1192 $boardData = $this->taskSanitizeAndValidate($request->get('board'), [
809 1193 'title' => 'required|string'
810 1194 ]);
1195 +
1196 + $boardData['source_board_id'] = $board_id;
1197 +
811 1198 $isWithLabels = $request->getSafe('isWithLabels');
812 1199 $isWithTasks = $request->getSafe('isWithTasks');
1200 + $isWithTemplates = $request->getSafe('isWithTemplates');
813 1201
814 1202 try {
815 1203 if(!PermissionManager::isAdmin()) {
816 1204 $errorMessage = __('You do not have permission to duplicate board', 'fluent-boards');
817 - throw new \Exception($errorMessage, 400);
1205 + throw new \Exception(esc_html($errorMessage), 400);
818 1206 }
819 1207 //create board
820 1208 $newBoard = $this->boardService->copyBoard($boardData);
821 1209
822 1210 //label copy
1211 + $labelMap = [];
1212 +
823 1213 if ($isWithLabels == 'yes') {
824 - $this->labelService->copyLabelsOfBoard($board_id, $newBoard);
1214 + $labelMap = $this->labelService->copyLabelsOfBoard($board_id, $newBoard);
825 1215 }
826 1216
827 1217 //stage copy
828 - $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id);
1218 + $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id, $isWithTemplates);
829 1219
830 1220 //copy tasks of selected stages
831 1221 if ($isWithTasks == 'yes') {
832 - $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard);
1222 + $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap,$isWithTemplates);
833 1223 }
834 1224
835 1225 return $this->sendSuccess([
836 1226 'board' => $newBoard,
@@ -842,11 +1232,18 @@
842 1232
843 1233 public function importFromBoard(Request $request, $board_id)
844 1234 {
845 1235 $selectedStages = $request->getSafe('selectedStages');
1236 + $position = $request->getSafe('position', 'intval');
846 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 +
847 1244 try {
848 - $this->stageService->importStagesFromBoard($board_id, $selectedStages);
1245 + $this->stageService->importStagesFromBoard($board_id, $selectedStages, $position);
849 1246
850 1247 return $this->sendSuccess([
851 1248 'message' => __('Import successfully', 'fluent-boards'),
852 1249 ], 200);
@@ -882,7 +1279,221 @@
882 1279 return [
883 1280 'message' => __('Board has been updated', 'fluent-boards'),
884 1281 'board' => apply_filters('fluent_boards/board_find', $board)
885 1282 ];
1283 + }
1284 +
1285 + public function archiveBoard($board_id)
1286 + {
1287 + try {
1288 + $board = $this->boardService->archiveBoard($board_id);
1289 +
1290 + return [
1291 + 'board' => $board,
1292 + 'message' => __('Board has been archived successfully!', 'fluent-boards')
1293 + ];
1294 + } catch (\Exception $e) {
1295 + return $this->sendError($e->getMessage(), 400);
1296 + }
1297 + }
1298 +
1299 + public function restoreBoard($board_id)
1300 + {
1301 + try {
1302 + $board = $this->boardService->restoreBoard($board_id);
1303 +
1304 + return [
1305 + 'board' => $board,
1306 + 'message' => __('Board has been restored successfully!', 'fluent-boards')
1307 + ];
1308 + } catch (\Exception $e) {
1309 + return $this->sendError($e->getMessage(), 400);
1310 + }
1311 + }
1312 +
1313 + private function boardUserRole($boardRelation)
1314 + {
1315 + return $boardRelation && Arr::get($boardRelation->settings, 'is_admin')
1316 + ? 'manager'
1317 + : ($boardRelation && Arr::has($boardRelation->settings, 'is_viewer_only') && Arr::get($boardRelation->settings, 'is_viewer_only')
1318 + ? 'viewer'
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;
886 1497 }
887 1498
888 1499 }