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 +781 -135 1.212.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');
@@ -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,46 @@
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 +
354 + $this->validateRequestedLabelPresets($request->get('labels'));
355 +
201 356 $board = $this->boardService->createBoard($boardData);
202 - $this->labelService->createDefaultLabel($board->id);
357 + $this->createBoardLabelsFromRequest($request, $board->id);
358 + $this->addBoardMembersFromRequest($request, $board->id);
203 359 $type = ucfirst($boardData['type']);
360 + $stages = $request->get('stages');
361 + $sanitizedStages = [];
204 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 +
205 373 if (isset($boardData['type']) && $boardData['type'] == 'roadmap') {
206 - $this->stageService->createRoadmapStages($board, $request->get('stages'));
374 + $this->stageService->createRoadmapStages($board, $sanitizedStages);
375 + } elseif (!empty($sanitizedStages)) {
376 + $this->stageService->createStages($board, $sanitizedStages);
207 377 } else {
208 378 $this->stageService->createDefaultStages($board);
209 379 }
210 380
@@ -214,37 +384,151 @@
214 384 }
215 385
216 386 do_action('fluent_boards/board_created', $board);
217 387
388 +
389 + if ($folderId) {
390 + $folderService->addBoardToFolder($folderId, [$board->id]);
391 + }
392 +
218 393 $message = __('Board has been created successfully', 'fluent-boards');
219 394
220 - return $this->sendSuccess([
395 + return $this->send([
221 396 'message' => $message,
222 397 'board' => $board,
223 398 ], 201);
224 399 } catch (\Exception $e) {
225 - return $this->sendError($e->getMessage(), 400);
400 + return $this->sendError([
401 + 'message' => $e->getMessage()
402 + ]);
226 403 }
227 404 }
228 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 + */
229 519 public function getArchivedStage(Request $request, $board_id)
230 520 {
231 521 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 - }
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 + ];
246 528
529 + $stages = $this->stageService->getArchivedStages($sanitizedParams, $board_id);
530 +
247 531 return $this->sendSuccess([
248 532 'stages' => $stages,
249 533 ], 200);
250 534 } catch (\Exception $e) {
@@ -251,17 +535,33 @@
251 535 return $this->sendError($e->getMessage(), 404);
252 536 }
253 537 }
254 538
255 - public function find($board_id)
539 + public function find(Request $request, $board_id)
256 540 {
257 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);
258 544 $board->background = maybe_unserialize($board->background);
259 545 $board->createdOn = $board->created_at->format('Y-m-d');
260 546
261 - $board->load(['users', 'stages', 'labels', 'owner']);
547 + $board->load(['users', 'labels', 'owner']);
262 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 +
263 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 +
264 564 $board->load(['customFields']);
265 565 }
266 566
267 567 $this->boardService->updateRecentBoards($board_id);
@@ -267,21 +567,35 @@
267 567 $this->boardService->updateRecentBoards($board_id);
268 568
269 569 $board->labelColor = Constant::TRELLO_COLOR_MAP;
270 570 $board->labelColorText = Constant::TEXT_COLOR_MAP;
571 + $board->labelColorPresets = Constant::LABEL_COLOR_PRESETS;
271 572
272 573 $board->users = Helper::sanitizeUserCollections($board->users);
273 574 $board->owner = Helper::sanitizeUserCollections($board->owner);
274 575
576 + $board->is_pinned = $this->boardService->isPinned($board->id);
577 +
275 578 $board = apply_filters('fluent_boards/board_find', $board);
276 579
277 580 return [
278 - 'board' => $board
581 + 'board' => $board,
582 + 'synced_at' => current_time('mysql')
279 583 ];
280 584 }
281 585
282 586 public function update(Request $request, $board_id)
283 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 +
284 598 $boardData = $this->boardSanitizeAndValidate($request->only(['title', 'description']), [
285 599 'title' => 'required|string',
286 600 'description' => 'nullable|string',
287 601 ]);
@@ -286,8 +600,9 @@
286 600 'description' => 'nullable|string',
287 601 ]);
288 602
289 603 $board = Board::findOrFail($board_id);
604 + $boardData['description'] = DescriptionMarkdownConverter::normalize($boardData['description']);
290 605
291 606 $oldBoard = clone $board;
292 607 $board->fill($boardData);
293 608 $board->save();
@@ -294,21 +609,23 @@
294 609
295 610 do_action('fluent_boards/board_updated', $board, $oldBoard);
296 611
297 612 return [
298 - 'stages' => $board->stages()->get(),
299 613 'message' => __('Board has been updated', 'fluent-boards'),
300 614 'board' => $board,
615 + 'stages' => $board->stages()->get(),
301 616 ];
302 617 }
303 618
304 619 public function archiveStage($board_id, $stage_id)
305 620 {
621 + $board_id = absint($board_id);
622 + $stage_id = absint($stage_id);
623 +
306 624 try {
307 - $stage = Stage::findOrFail($stage_id);
308 - $board = Board::findOrFail($stage->board_id);
625 + $stage = $this->findStageOnBoard($stage_id, $board_id);
309 626
310 - $updatedStage = $this->boardService->archiveStage($board->id, $stage);
627 + $updatedStage = $this->boardService->archiveStage($board_id, $stage);
311 628
312 629 return $this->sendSuccess([
313 630 'updatedStage' => $updatedStage,
314 631 'message' => __('Stage has been archived', 'fluent-boards'),
@@ -319,18 +636,20 @@
319 636 }
320 637
321 638 public function restoreStage($board_id, $stage_id)
322 639 {
640 + $board_id = absint($board_id);
641 + $stage_id = absint($stage_id);
642 +
323 643 try {
324 - $stage = Stage::findOrFail($stage_id);
325 - $board = Board::findOrFail($board_id);
644 + $stage = $this->findStageOnBoard($stage_id, $board_id);
326 645
327 - $updatedStage = $this->boardService->restoreStage($board->id, $stage);
646 + $updatedStage = $this->boardService->restoreStage($board_id, $stage);
328 647
329 648 return $this->sendSuccess([
330 - 'success' => true,
649 + 'success' => true,
331 650 'updatedStage' => $updatedStage,
332 - 'message' => __('Stage has been restored', 'fluent-boards')
651 + 'message' => __('Stage has been restored', 'fluent-boards')
333 652 ], 200);
334 653 } catch (\Exception $e) {
335 654 return $this->sendError($e->getMessage(), 400);
336 655 }
@@ -336,32 +655,22 @@
336 655 }
337 656 }
338 657
339 658
340 - public function changePositionOfStage(Request $request, $board_id)
659 + public function repositionStages(Request $request, $board_id)
341 660 {
342 - $changeData = $this->boardSanitizeAndValidate($request->only(['fromPosition', 'toPosition']), [
343 - 'fromPosition' => 'required',
344 - 'toPosition' => 'required',
345 - ]);
346 -
661 + $incomingList = $request->get('list');
662 + if (!is_array($incomingList)) {
663 + $incomingList = [];
664 + }
665 + $incomingList = array_map('intval', $incomingList);
347 666 try {
348 - $this->boardService->changePositionOfStage($board_id, $changeData);
667 + foreach ($incomingList as $stageId) {
668 + $this->findStageOnBoard($stageId, $board_id);
669 + }
349 670
671 + $this->boardService->repositionStages($board_id, $incomingList);
350 672 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 673 'message' => __('Stages Reordered', 'fluent-boards'),
365 674 'updatedStages' => $this->stageService->getLastOneMinuteUpdatedStages($board_id)
366 675 ], 200);
367 676 } catch (\Exception $e) {
@@ -379,9 +688,9 @@
379 688 public function delete($board_id)
380 689 {
381 690 try {
382 691 if (!PermissionManager::isAdmin()) {
383 - 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);
384 693 }
385 694 $this->boardService->deleteBoard($board_id);
386 695
387 696 return $this->sendSuccess([
@@ -399,9 +708,12 @@
399 708
400 709 public function getActivities(Request $request, $board_id)
401 710 {
402 711 try {
403 - $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 + ]);
404 716 return $this->sendSuccess([
405 717 'activities' => $activities,
406 718 ], 200);
407 719 } catch (\Exception $e) {
@@ -419,8 +731,11 @@
419 731 $boardObjects = Relation::where('object_type', 'board_user')
420 732 ->where('object_id', $board_id)
421 733 ->get()->keyBy('foreign_id');
422 734
735 + $superAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
736 + ->get()->pluck('object_id')->toArray();
737 +
423 738 $userIds = $boardObjects->pluck('foreign_id')->toArray();
424 739
425 740 $coreUsers = [];
426 741 if ($userIds) {
@@ -439,14 +754,17 @@
439 754 }
440 755
441 756 $boardRelation = $boardObjects[$user->ID] ?? null;
442 757
758 +
443 759 $formattedUsers[] = [
444 760 'ID' => $user->ID,
445 761 'display_name' => $name,
446 762 'email' => $user->user_email,
447 763 'photo' => fluent_boards_user_avatar($user->user_email, $name),
448 - 'role' => $boardRelation && $boardRelation->settings['is_admin'] ? 'manager' : 'member'
764 + 'role' => $this->boardUserRole($boardRelation),
765 + 'is_super' => in_array($user->ID, $superAdminIds),
766 + 'is_wpadmin' => $user->has_cap('manage_options')
449 767 ];
450 768 }
451 769
452 770 // order formatted users by display_name
@@ -463,16 +781,24 @@
463 781 return $returnData;
464 782 }
465 783
466 784 /*
467 - * 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.
468 786 */
469 - $adminUserIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
787 + $fluentBoardAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
470 788 ->whereNotIn('object_id', $userIds)
471 789 ->get()
472 790 ->pluck('object_id')
473 791 ->toArray();
474 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 +
475 801 if ($adminUserIds) {
476 802 $adminUsers = get_users([
477 803 'include' => $adminUserIds,
478 804 ]);
@@ -489,9 +815,11 @@
489 815 'ID' => $user->ID,
490 816 'display_name' => $name,
491 817 'email' => $user->user_email,
492 818 'photo' => fluent_boards_user_avatar($user->user_email, $name),
493 - 'role' => 'admin'
819 + 'role' => 'admin',
820 + 'is_super' => in_array($user->ID, $superAdminIds),
821 + 'is_wpadmin' => $user->has_cap('manage_options')
494 822 ];
495 823 }
496 824
497 825 // order formatted users by display_name
@@ -520,18 +848,25 @@
520 848 }
521 849
522 850 public function addMembersInBoard(Request $request, $board_id)
523 851 {
524 - $memberId = $request->getSafe('memberId');
525 - $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);
526 855
527 - if ($isAlreadyMember) {
856 + if ($member === null) {
528 857 return $this->sendError([
858 + 'message' => __('User not found.', 'fluent-boards'),
859 + ], 404);
860 + }
861 +
862 + if (!$member) {
863 + return $this->sendError([
529 864 'message' => __('User already a member', 'fluent-boards'),
530 - ], 304);
865 + ], 409);
531 866 }
532 - $member = $this->boardService->addMembersInBoard($board_id, $memberId);
533 867
868 +
534 869 return [
535 870 'message' => __('Member added successfully', 'fluent-boards'),
536 871 'member' => Helper::sanitizeUserCollections($member)
537 872 ];
@@ -559,11 +894,11 @@
559 894 }
560 895
561 896 public function searchBoards(Request $request)
562 897 {
563 - $per_page = $request->get('per_page', 10);
564 - $search_input = $request->searchInput . trim('');
565 - $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');
566 901
567 902 $currentUserId = get_current_user_id();
568 903
569 904 if (PermissionManager::isAdmin($currentUserId)) {
@@ -605,10 +940,13 @@
605 940 * @return
606 941 */
607 942 public function changeStageView($board_id, $stage_id)
608 943 {
944 + $board_id = absint($board_id);
945 + $stage_id = absint($stage_id);
946 +
609 947 try {
610 - $stage = Stage::findOrFail($stage_id);
948 + $stage = $this->findStageOnBoard($stage_id, $board_id);
611 949 $message = __('The stage is made public!', 'fluent-boards');
612 950 $settings = $stage->settings;
613 951
614 952 if (isset($settings['is_public'])) {
@@ -613,9 +951,9 @@
613 951
614 952 if (isset($settings['is_public'])) {
615 953 if ($settings['is_public']) {
616 954 $settings['is_public'] = false;
617 - $message = __('The stage is made admin only!', 'fluent-boards');
955 + $message = __('The stage is made private!', 'fluent-boards');
618 956 } else {
619 957 $settings['is_public'] = true;
620 958 }
621 959 } else {
@@ -634,24 +972,27 @@
634 972 }
635 973
636 974
637 975 /**
638 - * Set board background image or color
976 + * Set or reset board background image/color.
639 977 * @param \FluentBoards\Framework\Http\Request\Request $request
640 978 * @return
641 979 */
642 980 public function setBoardBackground(Request $request, $board_id)
643 981 {
644 - // sanitize and validate image_url
645 - 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) {
646 990 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
647 - "id" => 'required',
991 + 'id' => 'required|integer',
648 992 'image_url' => 'required|string|url',
649 993 ]);
650 - }
651 -
652 - // sanitize and validate color
653 - if ($request->color) {
994 + } elseif ($request->color) {
654 995 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
655 996 "id" => 'required',
656 997 'color' => 'required',
657 998 ]);
@@ -659,17 +1000,22 @@
659 1000
660 1001 try {
661 1002 if (!$board_id) {
662 1003 $errorMessage = __('Board id is required', 'fluent-boards');
663 - throw new \Exception($errorMessage, 400);
1004 + throw new \Exception(esc_html($errorMessage), 400);
664 1005 }
665 1006
1007 + if (empty($backgroundData)) {
1008 + $errorMessage = __('Background data is required', 'fluent-boards');
1009 + throw new \Exception(esc_html($errorMessage), 400);
1010 + }
1011 +
666 1012 return $this->sendSuccess([
667 1013 'message' => __('Background updated successfully', 'fluent-boards'),
668 1014 'background' => $this->boardService->setBoardBackground($backgroundData, $board_id),
669 1015 ]);
670 1016 } catch (\Exception $e) {
671 - $this->sendError([$e->getMessage(), 400]);
1017 + return $this->sendError($e->getMessage(), 400);
672 1018 }
673 1019 }
674 1020
675 1021
@@ -679,15 +1025,19 @@
679 1025 * @param mixed $stage_slug
680 1026 * @return $availablePositions as an array
681 1027 * @throws \Exception
682 1028 */
683 - public function getStageTaskAvailablePositions($board_id, $stage_id)
1029 + public function getStageTaskAvailablePositions(Request $request, $board_id, $stage_id)
684 1030 {
685 1031 try {
686 1032 if ($board_id && $stage_id) {
687 - $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);
688 1035 return $this->sendSuccess([
689 - 'availablePositions' => $availablePositions
1036 + 'availablePositions' => $availablePositions['availablePositions'],
1037 + 'moveTargets' => $availablePositions['moveTargets'],
1038 + 'currentMoveTargetKey' => $availablePositions['currentMoveTargetKey'],
1039 + 'defaultMoveTargetKey' => $availablePositions['defaultMoveTargetKey'],
690 1040 ], 200);
691 1041 } else {
692 1042 $message = '';
693 1043 if (!$board_id) {
@@ -695,12 +1045,12 @@
695 1045 }
696 1046 if (!$stage_id) {
697 1047 $message = 'Stage ';
698 1048 }
699 - throw new \Exception($message . 'is required', 400);
1049 + throw new \Exception(esc_html($message . 'is required'), 400);
700 1050 }
701 1051 } catch (\Exception $e) {
702 - $this->sendError([$e->getMessage(), 400]);
1052 + return $this->sendError($e->getMessage(), 400);
703 1053 }
704 1054 }
705 1055
706 1056 public function getAssociateCrmContacts($board_id)
@@ -709,24 +1059,47 @@
709 1059 $contactAssociatedTasks = Task::with('board')->where('board_id', $board_id)
710 1060 ->whereNotNull('crm_contact_id')
711 1061 ->get();
712 1062
713 - $formattedContacts = Collection::make($contactAssociatedTasks)
714 - ->groupBy('crm_contact_id')
715 - ->map(function ($tasks, $contactId) {
716 - $subscriber = Subscriber::find($contactId);
717 - 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) {
718 1092 return null; // Skip if subscriber not found
719 1093 }
720 1094
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 - ];
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;
729 1102 })
730 1103 ->filter()->toArray();
731 1104
732 1105
@@ -747,11 +1120,13 @@
747 1120 'message' => __('Associated Crm Member has been updated', 'fluent-boards'),
748 1121 ], 200);
749 1122 }
750 1123
751 - public function hasDataChanged($board_id)
1124 + public function hasDataChanged(Request $request, $board_id)
752 1125 {
753 - 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);
754 1129 }
755 1130
756 1131 public function createStage(Request $request, $board_id)
757 1132 {
@@ -756,8 +1131,9 @@
756 1131 public function createStage(Request $request, $board_id)
757 1132 {
758 1133 $stageData = $this->stageSanitizeAndValidate($request->all(), [
759 1134 'title' => 'required|string',
1135 + 'position' => 'nullable|numeric'
760 1136 ]);
761 1137
762 1138 $board = Board::find($board_id);
763 1139 $stage = $this->stageService->createStage($stageData, $board_id);
@@ -773,15 +1149,27 @@
773 1149 }
774 1150
775 1151 public function moveAllTasks(Request $request, $board_id)
776 1152 {
777 - $oldStageId = $request->getSafe('oldStageId');
778 - $newStageId = $request->getSafe('newStageId');
1153 + $oldStageId = $request->getSafe('oldStageId', 'intval');
1154 + $newStageId = $request->getSafe('newStageId', 'intval');
779 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 +
780 1168 $updates = $this->stageService->moveAllTasks($oldStageId, $newStageId, $board_id);
781 1169
782 1170 return [
783 - 'message' => __('Tasks has been Moved', 'fluent-boards'),
1171 + 'message' => __('Tasks have been moved', 'fluent-boards'),
784 1172 'updatedTasks' => $updates,
785 1173 ];
786 1174
787 1175 }
@@ -787,50 +1175,87 @@
787 1175 }
788 1176
789 1177 public function archiveAllTasksInStage($board_id, $stage_id)
790 1178 {
791 - $updates = $this->stageService->archiveAllTasksInStage($stage_id);
792 - return [
793 - 'message' => __('Tasks has been archived', 'fluent-boards'),
794 - 'updatedTasks' => $updates,
795 - ];
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 + }
796 1193 }
797 1194
798 1195 public function getAssociatedBoards(Request $request, $associated_id)
799 1196 {
800 - $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 +
801 1209 return [
802 1210 'boards' => $associatedBoards,
803 1211 ];
804 1212 }
805 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 +
806 1225 public function duplicateBoard(Request $request, $board_id)
807 1226 {
808 1227 $boardData = $this->taskSanitizeAndValidate($request->get('board'), [
809 1228 'title' => 'required|string'
810 1229 ]);
1230 +
1231 + $boardData['source_board_id'] = $board_id;
1232 +
811 1233 $isWithLabels = $request->getSafe('isWithLabels');
812 1234 $isWithTasks = $request->getSafe('isWithTasks');
1235 + $isWithTemplates = $request->getSafe('isWithTemplates');
813 1236
814 1237 try {
815 1238 if(!PermissionManager::isAdmin()) {
816 1239 $errorMessage = __('You do not have permission to duplicate board', 'fluent-boards');
817 - throw new \Exception($errorMessage, 400);
1240 + throw new \Exception(esc_html($errorMessage), 400);
818 1241 }
819 1242 //create board
820 1243 $newBoard = $this->boardService->copyBoard($boardData);
821 1244
822 1245 //label copy
1246 + $labelMap = [];
1247 +
823 1248 if ($isWithLabels == 'yes') {
824 - $this->labelService->copyLabelsOfBoard($board_id, $newBoard);
1249 + $labelMap = $this->labelService->copyLabelsOfBoard($board_id, $newBoard);
825 1250 }
826 1251
827 1252 //stage copy
828 - $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id);
1253 + $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id, $isWithTemplates);
829 1254
830 1255 //copy tasks of selected stages
831 1256 if ($isWithTasks == 'yes') {
832 - $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard);
1257 + $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap,$isWithTemplates);
833 1258 }
834 1259
835 1260 return $this->sendSuccess([
836 1261 'board' => $newBoard,
@@ -842,11 +1267,18 @@
842 1267
843 1268 public function importFromBoard(Request $request, $board_id)
844 1269 {
845 1270 $selectedStages = $request->getSafe('selectedStages');
1271 + $position = $request->getSafe('position', 'intval');
846 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 +
847 1279 try {
848 - $this->stageService->importStagesFromBoard($board_id, $selectedStages);
1280 + $this->stageService->importStagesFromBoard($board_id, $selectedStages, $position);
849 1281
850 1282 return $this->sendSuccess([
851 1283 'message' => __('Import successfully', 'fluent-boards'),
852 1284 ], 200);
@@ -882,7 +1314,221 @@
882 1314 return [
883 1315 'message' => __('Board has been updated', 'fluent-boards'),
884 1316 'board' => apply_filters('fluent_boards/board_find', $board)
885 1317 ];
1318 + }
1319 +
1320 + public function archiveBoard($board_id)
1321 + {
1322 + try {
1323 + $board = $this->boardService->archiveBoard($board_id);
1324 +
1325 + return [
1326 + 'board' => $board,
1327 + 'message' => __('Board has been archived successfully!', 'fluent-boards')
1328 + ];
1329 + } catch (\Exception $e) {
1330 + return $this->sendError($e->getMessage(), 400);
1331 + }
1332 + }
1333 +
1334 + public function restoreBoard($board_id)
1335 + {
1336 + try {
1337 + $board = $this->boardService->restoreBoard($board_id);
1338 +
1339 + return [
1340 + 'board' => $board,
1341 + 'message' => __('Board has been restored successfully!', 'fluent-boards')
1342 + ];
1343 + } catch (\Exception $e) {
1344 + return $this->sendError($e->getMessage(), 400);
1345 + }
1346 + }
1347 +
1348 + private function boardUserRole($boardRelation)
1349 + {
1350 + return $boardRelation && Arr::get($boardRelation->settings, 'is_admin')
1351 + ? 'manager'
1352 + : ($boardRelation && Arr::has($boardRelation->settings, 'is_viewer_only') && Arr::get($boardRelation->settings, 'is_viewer_only')
1353 + ? 'viewer'
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;
886 1532 }
887 1533
888 1534 }