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 +771 -135 1.232.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,14 +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 - $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 + }
59 78 } else {
60 - $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 + }
61 84 }
62 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
63 108 if (!empty($searchInput)) {
64 109 $relatedBoardsQuery = $relatedBoardsQuery->where('title', 'like', '%' . $searchInput . '%');
65 110 }
66 111
@@ -69,17 +114,59 @@
69 114 ->with('stages', 'users')
70 115 ->paginate($per_page);
71 116
72 117 foreach ($relatedBoards as $relatedBoard) {
118 + $relatedBoard->description = DescriptionMarkdownConverter::normalize($relatedBoard->description);
73 119 $relatedBoard->users = Helper::sanitizeUserCollections($relatedBoard->users);
120 + $relatedBoard->is_pinned = $this->boardService->isPinned($relatedBoard->id);
74 121 }
75 122
76 - return $this->sendSuccess([
77 - 'boards' => $relatedBoards
78 - ], 200);
123 + $response = [
124 + 'boards' => $relatedBoards,
125 + 'board_counts' => $this->boardService->getBoardCounts($userId)
126 + ];
127 +
128 + $folderMapping = $this->getBoardFolderMapping($userId);
129 + $response['folder_mapping'] = $folderMapping;
130 + if ($folderId) {
131 + $response['current_folder'] = isset($folderMapping[$folderId])
132 + ? $this->getCurrentFolderInfoFromMapping($folderMapping[$folderId])
133 + : null;
134 + }
135 +
136 + return $this->sendSuccess($response);
79 137 }
80 138
81 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 + /**
82 169 * Get the list of boards and their associated stages for the current user.
83 170 *
84 171 * @param \FluentBoards\Framework\Http\Request\Request $request
85 172 * @return \WP_REST_Response
@@ -85,30 +172,56 @@
85 172 * @return \WP_REST_Response
86 173 */
87 174 public function getBoardsList(Request $request)
88 175 {
176 + $userId = get_current_user_id();
177 +
178 + // Query to fetch boards that are not archived and accessible by the user
179 + // Check if the FLUENT_ROADMAP constant is defined
180 + if (!defined('FLUENT_ROADMAP')) {
181 + $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->excludeTemplates()->byAccessUser($userId);
182 + } else {
183 + $relatedBoardsQuery = Board::whereNull('archived_at')->excludeTemplates()->byAccessUser($userId);
184 + }
185 +
186 + $relatedBoards = $relatedBoardsQuery->with('stages')->get();
187 +
188 + // Fetch the stages associated with the boards
189 + $stages = Stage::whereIn('board_id', $relatedBoards->pluck('id'))->where('archived_at', null)->get();
190 +
191 + return $this->sendSuccess([
192 + 'boards' => $relatedBoards,
193 + 'all_stages' => $stages,
194 + ], 200);
195 + }
196 + public function getOnlyBoardsByUser(Request $request)
197 + {
89 198 try {
90 199 $userId = get_current_user_id();
91 200
92 - // Query to fetch boards that are not archived and accessible by the user
93 - // Check if the FLUENT_ROADMAP constant is defined
94 - if (!defined('FLUENT_ROADMAP')) {
95 - $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
201 + $searchInput = $request->getSafe('searchInput', 'sanitize_text_field');
202 +
203 +
204 + if(!defined('FLUENT_ROADMAP'))
205 + {
206 + $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->excludeTemplates()->byAccessUser($userId);
96 207 } else {
97 - $relatedBoardsQuery = Board::whereNull('archived_at')->byAccessUser($userId);
208 + $relatedBoardsQuery = Board::whereNull('archived_at')->excludeTemplates()->byAccessUser($userId);
98 209 }
99 210
100 - $relatedBoards = $relatedBoardsQuery->with('stages')->get();
211 + if (!empty($searchInput)) {
212 + $relatedBoardsQuery = $relatedBoardsQuery->where('title', 'like', '%' . $searchInput . '%');
213 + }
101 214
102 - // Fetch the stages associated with the boards
103 - $stages = Stage::whereIn('board_id', $relatedBoards->pluck('id'))->where('archived_at', null)->get();
215 + $relatedBoards = $relatedBoardsQuery->orderBy('created_at', 'DESC')->get();
104 216
105 217 return $this->sendSuccess([
106 - 'boards' => $relatedBoards,
107 - 'all_stages' => $stages,
108 - ], 200);
218 + 'boards' => $relatedBoards
219 + ]);
109 220 } catch (\Exception $e) {
110 - return $this->sendError($e->getMessage(), 404);
221 + return $this->sendError([
222 + 'message' => $e->getMessage()
223 + ]);
111 224 }
112 225 }
113 226
114 227 public function getRecentBoards()
@@ -115,9 +228,12 @@
115 228 {
116 229 $boards = $this->boardService->getRecentBoards();
117 230
118 231 if (!$boards || $boards->isEmpty()) {
119 - $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())
120 236 ->limit(4)
121 237 ->withCount('completedTasks')
122 238 ->with(['stages', 'users'])
123 239 ->get();
@@ -124,8 +240,9 @@
124 240 }
125 241
126 242 foreach ($boards as $board) {
127 243 $board->users = Helper::sanitizeUserCollections($board->users);
244 + $board->is_pinned = $this->boardService->isPinned($board->id);
128 245 }
129 246
130 247 return [
131 248 'boards' => $boards,
@@ -140,9 +257,9 @@
140 257 $boards = $this->boardService->getBoardsByType($type);
141 258
142 259 return $this->sendSuccess([
143 260 'boards' => $boards,
144 - ], 200);
261 + ]);
145 262 }
146 263
147 264 public function createFirstBoard(Request $request)
148 265 {
@@ -153,11 +270,14 @@
153 270 'currency' => 'nullable|string',
154 271 'crm_contact_id' => 'nullable|numeric',
155 272 ]);
156 273
157 - $installFluentCRM = $request->get('withFluentCRM') == 'yes' ? true : false;
274 + $installFluentCRM = $request->getSafe('withFluentCRM', 'sanitize_text_field') == 'yes' ? true : false;
158 275
159 276 $postStages = $request->get('stages');
277 + if (!is_array($postStages)) {
278 + $postStages = [];
279 + }
160 280 $stageData = array();
161 281 foreach ($postStages as $stage) {
162 282 $temp = $this->stageSanitizeAndValidate($stage, [
163 283 'title' => 'required|string',
@@ -167,9 +287,9 @@
167 287
168 288 $taskData = null;
169 289 if ($request->get('task')) {
170 290 $taskData = $this->taskSanitizeAndValidate($request->get('task'), [
171 - 'title' => 'required|string'
291 + 'title' => 'required|string',
172 292 ]);
173 293 }
174 294
175 295 $board = $this->boardService->createBoard($boardData);
@@ -183,9 +303,9 @@
183 303 $this->taskService->createTask($taskData, $board->id);
184 304 }
185 305
186 306 do_action('fluent_boards/board_created', $board);
187 -
307 +
188 308 if ($installFluentCRM && !defined('FLUENTCRM')) {
189 309 InstallService::install('fluent-crm');
190 310 }
191 311
@@ -194,8 +314,21 @@
194 314 'board' => $board,
195 315 ];
196 316 }
197 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 +
198 331 public function create(Request $request)
199 332 {
200 333 $boardData = $this->boardSanitizeAndValidate($request->get('board'), [
201 334 'title' => 'required|string',
@@ -202,17 +335,46 @@
202 335 'description' => 'nullable',
203 336 'type' => 'required|string',
204 337 'currency' => 'nullable|string',
205 338 'crm_contact_id' => 'nullable|numeric',
339 + 'folder_id' => 'nullable',
206 340 ]);
207 341
208 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 +
209 356 $board = $this->boardService->createBoard($boardData);
210 - $this->labelService->createDefaultLabel($board->id);
357 + $this->createBoardLabelsFromRequest($request, $board->id);
358 + $this->addBoardMembersFromRequest($request, $board->id);
211 359 $type = ucfirst($boardData['type']);
360 + $stages = $request->get('stages');
361 + $sanitizedStages = [];
212 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 +
213 373 if (isset($boardData['type']) && $boardData['type'] == 'roadmap') {
214 - $this->stageService->createRoadmapStages($board, $request->get('stages'));
374 + $this->stageService->createRoadmapStages($board, $sanitizedStages);
375 + } elseif (!empty($sanitizedStages)) {
376 + $this->stageService->createStages($board, $sanitizedStages);
215 377 } else {
216 378 $this->stageService->createDefaultStages($board);
217 379 }
218 380
@@ -222,37 +384,151 @@
222 384 }
223 385
224 386 do_action('fluent_boards/board_created', $board);
225 387
388 +
389 + if ($folderId) {
390 + $folderService->addBoardToFolder($folderId, [$board->id]);
391 + }
392 +
226 393 $message = __('Board has been created successfully', 'fluent-boards');
227 394
228 - return $this->sendSuccess([
395 + return $this->send([
229 396 'message' => $message,
230 397 'board' => $board,
231 398 ], 201);
232 399 } catch (\Exception $e) {
233 - return $this->sendError($e->getMessage(), 400);
400 + return $this->sendError([
401 + 'message' => $e->getMessage()
402 + ]);
234 403 }
235 404 }
236 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 + */
237 519 public function getArchivedStage(Request $request, $board_id)
238 520 {
239 521 try {
240 - $pagination = $request->noPagination ? true : false;
241 - $per_page = isset($data['per_page']) ? $data['per_page'] : 30;
242 - $page = isset($data['page']) ? $data['page'] : 1;
243 - if ($pagination) {
244 - $stages = Stage::where('board_id', $board_id)
245 - ->whereNotNull('archived_at')
246 - ->orderBy('created_at', 'DESC')
247 - ->get();
248 - } else {
249 - $stages = Stage::where('board_id', $board_id)
250 - ->whereNotNull('archived_at')
251 - ->orderBy('created_at', 'DESC')
252 - ->paginate($per_page, ['*'], 'page', $page);
253 - }
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 + ];
254 528
529 + $stages = $this->stageService->getArchivedStages($sanitizedParams, $board_id);
530 +
255 531 return $this->sendSuccess([
256 532 'stages' => $stages,
257 533 ], 200);
258 534 } catch (\Exception $e) {
@@ -259,17 +535,33 @@
259 535 return $this->sendError($e->getMessage(), 404);
260 536 }
261 537 }
262 538
263 - public function find($board_id)
539 + public function find(Request $request, $board_id)
264 540 {
265 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);
266 544 $board->background = maybe_unserialize($board->background);
267 545 $board->createdOn = $board->created_at->format('Y-m-d');
268 546
269 - $board->load(['users', 'stages', 'labels', 'owner']);
547 + $board->load(['users', 'labels', 'owner']);
270 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 +
271 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 +
272 564 $board->load(['customFields']);
273 565 }
274 566
275 567 $this->boardService->updateRecentBoards($board_id);
@@ -275,21 +567,35 @@
275 567 $this->boardService->updateRecentBoards($board_id);
276 568
277 569 $board->labelColor = Constant::TRELLO_COLOR_MAP;
278 570 $board->labelColorText = Constant::TEXT_COLOR_MAP;
571 + $board->labelColorPresets = Constant::LABEL_COLOR_PRESETS;
279 572
280 573 $board->users = Helper::sanitizeUserCollections($board->users);
281 574 $board->owner = Helper::sanitizeUserCollections($board->owner);
282 575
576 + $board->is_pinned = $this->boardService->isPinned($board->id);
577 +
283 578 $board = apply_filters('fluent_boards/board_find', $board);
284 579
285 580 return [
286 - 'board' => $board
581 + 'board' => $board,
582 + 'synced_at' => current_time('mysql')
287 583 ];
288 584 }
289 585
290 586 public function update(Request $request, $board_id)
291 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 +
292 598 $boardData = $this->boardSanitizeAndValidate($request->only(['title', 'description']), [
293 599 'title' => 'required|string',
294 600 'description' => 'nullable|string',
295 601 ]);
@@ -294,8 +600,9 @@
294 600 'description' => 'nullable|string',
295 601 ]);
296 602
297 603 $board = Board::findOrFail($board_id);
604 + $boardData['description'] = DescriptionMarkdownConverter::normalize($boardData['description']);
298 605
299 606 $oldBoard = clone $board;
300 607 $board->fill($boardData);
301 608 $board->save();
@@ -302,21 +609,23 @@
302 609
303 610 do_action('fluent_boards/board_updated', $board, $oldBoard);
304 611
305 612 return [
306 - 'stages' => $board->stages()->get(),
307 613 'message' => __('Board has been updated', 'fluent-boards'),
308 614 'board' => $board,
615 + 'stages' => $board->stages()->get(),
309 616 ];
310 617 }
311 618
312 619 public function archiveStage($board_id, $stage_id)
313 620 {
621 + $board_id = absint($board_id);
622 + $stage_id = absint($stage_id);
623 +
314 624 try {
315 - $stage = Stage::findOrFail($stage_id);
316 - $board = Board::findOrFail($stage->board_id);
625 + $stage = $this->findStageOnBoard($stage_id, $board_id);
317 626
318 - $updatedStage = $this->boardService->archiveStage($board->id, $stage);
627 + $updatedStage = $this->boardService->archiveStage($board_id, $stage);
319 628
320 629 return $this->sendSuccess([
321 630 'updatedStage' => $updatedStage,
322 631 'message' => __('Stage has been archived', 'fluent-boards'),
@@ -327,18 +636,20 @@
327 636 }
328 637
329 638 public function restoreStage($board_id, $stage_id)
330 639 {
640 + $board_id = absint($board_id);
641 + $stage_id = absint($stage_id);
642 +
331 643 try {
332 - $stage = Stage::findOrFail($stage_id);
333 - $board = Board::findOrFail($board_id);
644 + $stage = $this->findStageOnBoard($stage_id, $board_id);
334 645
335 - $updatedStage = $this->boardService->restoreStage($board->id, $stage);
646 + $updatedStage = $this->boardService->restoreStage($board_id, $stage);
336 647
337 648 return $this->sendSuccess([
338 - 'success' => true,
649 + 'success' => true,
339 650 'updatedStage' => $updatedStage,
340 - 'message' => __('Stage has been restored', 'fluent-boards')
651 + 'message' => __('Stage has been restored', 'fluent-boards')
341 652 ], 200);
342 653 } catch (\Exception $e) {
343 654 return $this->sendError($e->getMessage(), 400);
344 655 }
@@ -344,32 +655,22 @@
344 655 }
345 656 }
346 657
347 658
348 - public function changePositionOfStage(Request $request, $board_id)
659 + public function repositionStages(Request $request, $board_id)
349 660 {
350 - $changeData = $this->boardSanitizeAndValidate($request->only(['fromPosition', 'toPosition']), [
351 - 'fromPosition' => 'required',
352 - 'toPosition' => 'required',
353 - ]);
354 -
661 + $incomingList = $request->get('list');
662 + if (!is_array($incomingList)) {
663 + $incomingList = [];
664 + }
665 + $incomingList = array_map('intval', $incomingList);
355 666 try {
356 - $this->boardService->changePositionOfStage($board_id, $changeData);
667 + foreach ($incomingList as $stageId) {
668 + $this->findStageOnBoard($stageId, $board_id);
669 + }
357 670
671 + $this->boardService->repositionStages($board_id, $incomingList);
358 672 return $this->sendSuccess([
359 - 'message' => __('Board stage has been updated', 'fluent-boards')
360 - ], 200);
361 - } catch (\Exception $e) {
362 - return $this->sendError($e->getMessage(), 400);
363 - }
364 - }
365 -
366 - public function rePositionStages(Request $request, $board_id)
367 - {
368 - $incomingList = $request->get('list');
369 - try {
370 - $this->boardService->rePositionStages($board_id, $incomingList);
371 - return $this->sendSuccess([
372 673 'message' => __('Stages Reordered', 'fluent-boards'),
373 674 'updatedStages' => $this->stageService->getLastOneMinuteUpdatedStages($board_id)
374 675 ], 200);
375 676 } catch (\Exception $e) {
@@ -387,9 +688,9 @@
387 688 public function delete($board_id)
388 689 {
389 690 try {
390 691 if (!PermissionManager::isAdmin()) {
391 - 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);
392 693 }
393 694 $this->boardService->deleteBoard($board_id);
394 695
395 696 return $this->sendSuccess([
@@ -407,9 +708,12 @@
407 708
408 709 public function getActivities(Request $request, $board_id)
409 710 {
410 711 try {
411 - $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 + ]);
412 716 return $this->sendSuccess([
413 717 'activities' => $activities,
414 718 ], 200);
415 719 } catch (\Exception $e) {
@@ -427,8 +731,11 @@
427 731 $boardObjects = Relation::where('object_type', 'board_user')
428 732 ->where('object_id', $board_id)
429 733 ->get()->keyBy('foreign_id');
430 734
735 + $superAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
736 + ->get()->pluck('object_id')->toArray();
737 +
431 738 $userIds = $boardObjects->pluck('foreign_id')->toArray();
432 739
433 740 $coreUsers = [];
434 741 if ($userIds) {
@@ -447,14 +754,17 @@
447 754 }
448 755
449 756 $boardRelation = $boardObjects[$user->ID] ?? null;
450 757
758 +
451 759 $formattedUsers[] = [
452 760 'ID' => $user->ID,
453 761 'display_name' => $name,
454 762 'email' => $user->user_email,
455 763 'photo' => fluent_boards_user_avatar($user->user_email, $name),
456 - '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')
457 767 ];
458 768 }
459 769
460 770 // order formatted users by display_name
@@ -471,16 +781,24 @@
471 781 return $returnData;
472 782 }
473 783
474 784 /*
475 - * 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.
476 786 */
477 - $adminUserIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
787 + $fluentBoardAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
478 788 ->whereNotIn('object_id', $userIds)
479 789 ->get()
480 790 ->pluck('object_id')
481 791 ->toArray();
482 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 +
483 801 if ($adminUserIds) {
484 802 $adminUsers = get_users([
485 803 'include' => $adminUserIds,
486 804 ]);
@@ -497,9 +815,11 @@
497 815 'ID' => $user->ID,
498 816 'display_name' => $name,
499 817 'email' => $user->user_email,
500 818 'photo' => fluent_boards_user_avatar($user->user_email, $name),
501 - 'role' => 'admin'
819 + 'role' => 'admin',
820 + 'is_super' => in_array($user->ID, $superAdminIds),
821 + 'is_wpadmin' => $user->has_cap('manage_options')
502 822 ];
503 823 }
504 824
505 825 // order formatted users by display_name
@@ -528,18 +848,25 @@
528 848 }
529 849
530 850 public function addMembersInBoard(Request $request, $board_id)
531 851 {
532 - $memberId = $request->getSafe('memberId');
533 - $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);
534 855
535 - if ($isAlreadyMember) {
856 + if ($member === null) {
536 857 return $this->sendError([
858 + 'message' => __('User not found.', 'fluent-boards'),
859 + ], 404);
860 + }
861 +
862 + if (!$member) {
863 + return $this->sendError([
537 864 'message' => __('User already a member', 'fluent-boards'),
538 - ], 304);
865 + ], 409);
539 866 }
540 - $member = $this->boardService->addMembersInBoard($board_id, $memberId);
541 867
868 +
542 869 return [
543 870 'message' => __('Member added successfully', 'fluent-boards'),
544 871 'member' => Helper::sanitizeUserCollections($member)
545 872 ];
@@ -567,11 +894,11 @@
567 894 }
568 895
569 896 public function searchBoards(Request $request)
570 897 {
571 - $per_page = $request->get('per_page', 10);
572 - $search_input = $request->searchInput . trim('');
573 - $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');
574 901
575 902 $currentUserId = get_current_user_id();
576 903
577 904 if (PermissionManager::isAdmin($currentUserId)) {
@@ -613,10 +940,13 @@
613 940 * @return
614 941 */
615 942 public function changeStageView($board_id, $stage_id)
616 943 {
944 + $board_id = absint($board_id);
945 + $stage_id = absint($stage_id);
946 +
617 947 try {
618 - $stage = Stage::findOrFail($stage_id);
948 + $stage = $this->findStageOnBoard($stage_id, $board_id);
619 949 $message = __('The stage is made public!', 'fluent-boards');
620 950 $settings = $stage->settings;
621 951
622 952 if (isset($settings['is_public'])) {
@@ -621,9 +951,9 @@
621 951
622 952 if (isset($settings['is_public'])) {
623 953 if ($settings['is_public']) {
624 954 $settings['is_public'] = false;
625 - $message = __('The stage is made admin only!', 'fluent-boards');
955 + $message = __('The stage is made private!', 'fluent-boards');
626 956 } else {
627 957 $settings['is_public'] = true;
628 958 }
629 959 } else {
@@ -642,24 +972,27 @@
642 972 }
643 973
644 974
645 975 /**
646 - * Set board background image or color
976 + * Set or reset board background image/color.
647 977 * @param \FluentBoards\Framework\Http\Request\Request $request
648 978 * @return
649 979 */
650 980 public function setBoardBackground(Request $request, $board_id)
651 981 {
652 - // sanitize and validate image_url
653 - 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) {
654 990 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
655 - "id" => 'required',
991 + 'id' => 'required|integer',
656 992 'image_url' => 'required|string|url',
657 993 ]);
658 - }
659 -
660 - // sanitize and validate color
661 - if ($request->color) {
994 + } elseif ($request->color) {
662 995 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
663 996 "id" => 'required',
664 997 'color' => 'required',
665 998 ]);
@@ -667,17 +1000,22 @@
667 1000
668 1001 try {
669 1002 if (!$board_id) {
670 1003 $errorMessage = __('Board id is required', 'fluent-boards');
671 - throw new \Exception($errorMessage, 400);
1004 + throw new \Exception(esc_html($errorMessage), 400);
672 1005 }
673 1006
1007 + if (empty($backgroundData)) {
1008 + $errorMessage = __('Background data is required', 'fluent-boards');
1009 + throw new \Exception(esc_html($errorMessage), 400);
1010 + }
1011 +
674 1012 return $this->sendSuccess([
675 1013 'message' => __('Background updated successfully', 'fluent-boards'),
676 1014 'background' => $this->boardService->setBoardBackground($backgroundData, $board_id),
677 1015 ]);
678 1016 } catch (\Exception $e) {
679 - $this->sendError([$e->getMessage(), 400]);
1017 + return $this->sendError($e->getMessage(), 400);
680 1018 }
681 1019 }
682 1020
683 1021
@@ -687,15 +1025,19 @@
687 1025 * @param mixed $stage_slug
688 1026 * @return $availablePositions as an array
689 1027 * @throws \Exception
690 1028 */
691 - public function getStageTaskAvailablePositions($board_id, $stage_id)
1029 + public function getStageTaskAvailablePositions(Request $request, $board_id, $stage_id)
692 1030 {
693 1031 try {
694 1032 if ($board_id && $stage_id) {
695 - $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);
696 1035 return $this->sendSuccess([
697 - 'availablePositions' => $availablePositions
1036 + 'availablePositions' => $availablePositions['availablePositions'],
1037 + 'moveTargets' => $availablePositions['moveTargets'],
1038 + 'currentMoveTargetKey' => $availablePositions['currentMoveTargetKey'],
1039 + 'defaultMoveTargetKey' => $availablePositions['defaultMoveTargetKey'],
698 1040 ], 200);
699 1041 } else {
700 1042 $message = '';
701 1043 if (!$board_id) {
@@ -703,12 +1045,12 @@
703 1045 }
704 1046 if (!$stage_id) {
705 1047 $message = 'Stage ';
706 1048 }
707 - throw new \Exception($message . 'is required', 400);
1049 + throw new \Exception(esc_html($message . 'is required'), 400);
708 1050 }
709 1051 } catch (\Exception $e) {
710 - $this->sendError([$e->getMessage(), 400]);
1052 + return $this->sendError($e->getMessage(), 400);
711 1053 }
712 1054 }
713 1055
714 1056 public function getAssociateCrmContacts($board_id)
@@ -717,24 +1059,47 @@
717 1059 $contactAssociatedTasks = Task::with('board')->where('board_id', $board_id)
718 1060 ->whereNotNull('crm_contact_id')
719 1061 ->get();
720 1062
721 - $formattedContacts = Collection::make($contactAssociatedTasks)
722 - ->groupBy('crm_contact_id')
723 - ->map(function ($tasks, $contactId) {
724 - $subscriber = Subscriber::find($contactId);
725 - 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) {
726 1092 return null; // Skip if subscriber not found
727 1093 }
728 1094
729 - return [
730 - 'name' => $subscriber->first_name . ' ' . $subscriber->last_name,
731 - 'photo' => $subscriber->photo,
732 - 'email' => $subscriber->email,
733 - 'crm_contact_id' => $contactId,
734 - 'id' => $contactId,
735 - 'tasks' => $tasks,
736 - ];
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;
737 1102 })
738 1103 ->filter()->toArray();
739 1104
740 1105
@@ -755,11 +1120,13 @@
755 1120 'message' => __('Associated Crm Member has been updated', 'fluent-boards'),
756 1121 ], 200);
757 1122 }
758 1123
759 - public function hasDataChanged($board_id)
1124 + public function hasDataChanged(Request $request, $board_id)
760 1125 {
761 - 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);
762 1129 }
763 1130
764 1131 public function createStage(Request $request, $board_id)
765 1132 {
@@ -764,8 +1131,9 @@
764 1131 public function createStage(Request $request, $board_id)
765 1132 {
766 1133 $stageData = $this->stageSanitizeAndValidate($request->all(), [
767 1134 'title' => 'required|string',
1135 + 'position' => 'nullable|numeric'
768 1136 ]);
769 1137
770 1138 $board = Board::find($board_id);
771 1139 $stage = $this->stageService->createStage($stageData, $board_id);
@@ -781,15 +1149,27 @@
781 1149 }
782 1150
783 1151 public function moveAllTasks(Request $request, $board_id)
784 1152 {
785 - $oldStageId = $request->getSafe('oldStageId');
786 - $newStageId = $request->getSafe('newStageId');
1153 + $oldStageId = $request->getSafe('oldStageId', 'intval');
1154 + $newStageId = $request->getSafe('newStageId', 'intval');
787 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 +
788 1168 $updates = $this->stageService->moveAllTasks($oldStageId, $newStageId, $board_id);
789 1169
790 1170 return [
791 - 'message' => __('Tasks has been Moved', 'fluent-boards'),
1171 + 'message' => __('Tasks have been moved', 'fluent-boards'),
792 1172 'updatedTasks' => $updates,
793 1173 ];
794 1174
795 1175 }
@@ -795,35 +1175,70 @@
795 1175 }
796 1176
797 1177 public function archiveAllTasksInStage($board_id, $stage_id)
798 1178 {
799 - $updates = $this->stageService->archiveAllTasksInStage($stage_id);
800 - return [
801 - 'message' => __('Tasks has been archived', 'fluent-boards'),
802 - 'updatedTasks' => $updates,
803 - ];
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 + }
804 1193 }
805 1194
806 1195 public function getAssociatedBoards(Request $request, $associated_id)
807 1196 {
808 - $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 +
809 1209 return [
810 1210 'boards' => $associatedBoards,
811 1211 ];
812 1212 }
813 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 +
814 1225 public function duplicateBoard(Request $request, $board_id)
815 1226 {
816 1227 $boardData = $this->taskSanitizeAndValidate($request->get('board'), [
817 1228 'title' => 'required|string'
818 1229 ]);
1230 +
1231 + $boardData['source_board_id'] = $board_id;
1232 +
819 1233 $isWithLabels = $request->getSafe('isWithLabels');
820 1234 $isWithTasks = $request->getSafe('isWithTasks');
1235 + $isWithTemplates = $request->getSafe('isWithTemplates');
821 1236
822 1237 try {
823 1238 if(!PermissionManager::isAdmin()) {
824 1239 $errorMessage = __('You do not have permission to duplicate board', 'fluent-boards');
825 - throw new \Exception($errorMessage, 400);
1240 + throw new \Exception(esc_html($errorMessage), 400);
826 1241 }
827 1242 //create board
828 1243 $newBoard = $this->boardService->copyBoard($boardData);
829 1244
@@ -834,13 +1249,13 @@
834 1249 $labelMap = $this->labelService->copyLabelsOfBoard($board_id, $newBoard);
835 1250 }
836 1251
837 1252 //stage copy
838 - $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id);
1253 + $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id, $isWithTemplates);
839 1254
840 1255 //copy tasks of selected stages
841 1256 if ($isWithTasks == 'yes') {
842 - $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap);
1257 + $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap,$isWithTemplates);
843 1258 }
844 1259
845 1260 return $this->sendSuccess([
846 1261 'board' => $newBoard,
@@ -852,11 +1267,18 @@
852 1267
853 1268 public function importFromBoard(Request $request, $board_id)
854 1269 {
855 1270 $selectedStages = $request->getSafe('selectedStages');
1271 + $position = $request->getSafe('position', 'intval');
856 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 +
857 1279 try {
858 - $this->stageService->importStagesFromBoard($board_id, $selectedStages);
1280 + $this->stageService->importStagesFromBoard($board_id, $selectedStages, $position);
859 1281
860 1282 return $this->sendSuccess([
861 1283 'message' => __('Import successfully', 'fluent-boards'),
862 1284 ], 200);
@@ -892,7 +1314,221 @@
892 1314 return [
893 1315 'message' => __('Board has been updated', 'fluent-boards'),
894 1316 'board' => apply_filters('fluent_boards/board_find', $board)
895 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;
896 1532 }
897 1533
898 1534 }