PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.2.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.2.0
2.2.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 All 43 releases
← All changes | app/Http/Controllers/BoardController.php +808 -136 1.21 → 2.2.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,23 +55,66 @@
45 55 }
46 56
47 57 public function getBoards(Request $request)
48 58 {
49 - $per_page = $request->getSafe('per_page', 'intval', 20);
59 + $per_page = $request->getSafe('per_page', 'intval', 100);
60 + $per_page = max(1, min(100, $per_page));
50 61 $userId = get_current_user_id();
51 62 $type = $request->getSafe('type', 'sanitize_text_field', 'to-do');
52 63
53 64 $order = $request->getSafe('order', 'sanitize_text_field', 'created_at');
54 65 $orderBy = $request->getSafe('orderBy', 'sanitize_text_field', 'DESC');
66 + $order = is_string($order) ? sanitize_sql_orderby($order) : false;
67 + if (!in_array($order, ['created_at', 'title', 'updated_at', 'id'], true)) {
68 + $order = 'created_at';
69 + }
70 + $orderBy = is_string($orderBy) ? strtoupper($orderBy) : 'DESC';
71 + if (!in_array($orderBy, ['ASC', 'DESC'], true)) {
72 + $orderBy = 'DESC';
73 + }
74 +
55 75 $searchInput = $request->getSafe('searchInput', 'sanitize_text_field');
56 76
57 - if(!defined('FLUENT_ROADMAP'))
58 - {
59 - $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
77 + $option = $request->getSafe('option', 'sanitize_text_field');
78 + $folderId = $request->getSafe('fid', 'intval'); // Get folder ID from request
79 +
80 + // Initialize the query based on archive status
81 + if (!defined('FLUENT_ROADMAP')) {
82 + if ($option == 'archived') {
83 + $relatedBoardsQuery = Board::whereNotNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
84 + } else {
85 + $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
86 + }
60 87 } else {
61 - $relatedBoardsQuery = Board::whereNull('archived_at')->byAccessUser($userId);
88 + if ($option == 'archived') {
89 + $relatedBoardsQuery = Board::whereNotNull('archived_at')->byAccessUser($userId);
90 + } else {
91 + $relatedBoardsQuery = Board::whereNull('archived_at')->byAccessUser($userId);
92 + }
62 93 }
63 94
95 + // Scope pinned before pagination, from the same id source as getBoardCounts(),
96 + // so the pinned page and board_counts.pinned always agree. Filtering pinned
97 + // after pagination would drop pinned boards that fall on a later active page.
98 + $pinnedIds = $this->boardService->getPinnedBoardIds();
99 + if ($option == 'pinned') {
100 +
101 + $relatedBoardsQuery = $pinnedIds
102 + ? $relatedBoardsQuery->whereIn('id', $pinnedIds)
103 + : $relatedBoardsQuery->where('id', 0);
104 + }
105 +
106 + if ($folderId) {
107 + $boardIds = (new FolderService())->getBoardIdsByFolder($folderId);
108 + $relatedBoardsQuery = $boardIds
109 + ? $relatedBoardsQuery->whereIn('id', $boardIds)
110 + : $relatedBoardsQuery->where('id', 0);
111 + }
112 +
113 + // Filter out boards that are templates (exclude boards where settings->is_template is true)
114 + $relatedBoardsQuery = $relatedBoardsQuery->excludeTemplates();
115 +
116 + // Add search functionality
64 117 if (!empty($searchInput)) {
65 118 $relatedBoardsQuery = $relatedBoardsQuery->where('title', 'like', '%' . $searchInput . '%');
66 119 }
67 120
@@ -66,41 +119,118 @@
66 119 }
67 120
68 121 $relatedBoards = $relatedBoardsQuery->orderBy($order, $orderBy)
69 122 ->withCount('completedTasks')
70 - ->with('stages', 'users')
123 + ->with('stages', 'users', 'boardMeta', 'viewerMembership')
71 124 ->paginate($per_page);
72 125
73 126 foreach ($relatedBoards as $relatedBoard) {
127 + $relatedBoard->description = DescriptionMarkdownConverter::normalize($relatedBoard->description);
74 128 $relatedBoard->users = Helper::sanitizeUserCollections($relatedBoard->users);
129 + $relatedBoard->is_pinned = in_array((int) $relatedBoard->id, $pinnedIds, true);
75 130 }
76 131
132 + $response = [
133 + 'boards' => $relatedBoards,
134 + 'board_counts' => $this->boardService->getBoardCounts($userId)
135 + ];
136 +
137 + $folderMapping = $this->getBoardFolderMapping($userId);
138 + $response['folder_mapping'] = $folderMapping;
139 + if ($folderId) {
140 + $response['current_folder'] = isset($folderMapping[$folderId])
141 + ? $this->getCurrentFolderInfoFromMapping($folderMapping[$folderId])
142 + : null;
143 + }
144 +
145 + return $this->sendSuccess($response);
146 + }
147 +
148 + /**
149 + * Get folder mapping for boards
150 + */
151 + private function getBoardFolderMapping($userId)
152 + {
153 + $folderService = new FolderService();
154 + $folders = $folderService->getFolders($userId);
155 +
156 + $mapping = [];
157 + foreach ($folders as $folder) {
158 + $mapping[$folder->id] = [
159 + 'id' => $folder->id,
160 + 'title' => $folder->title,
161 + 'board_ids' => $folder->boards ? $folder->boards->pluck('id')->toArray() : []
162 + ];
163 + }
164 +
165 + return $mapping;
166 + }
167 +
168 + private function getCurrentFolderInfoFromMapping(array $folder)
169 + {
170 + return [
171 + 'id' => $folder['id'],
172 + 'title' => $folder['title'],
173 + 'board_count' => count($folder['board_ids'])
174 + ];
175 + }
176 +
177 + /**
178 + * Get the list of boards and their associated stages for the current user.
179 + *
180 + * @param \FluentBoards\Framework\Http\Request\Request $request
181 + * @return \WP_REST_Response
182 + */
183 + public function getBoardsList(Request $request)
184 + {
185 + $userId = get_current_user_id();
186 +
187 + // Query to fetch boards that are not archived and accessible by the user
188 + // Check if the FLUENT_ROADMAP constant is defined
189 + if (!defined('FLUENT_ROADMAP')) {
190 + $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->excludeTemplates()->byAccessUser($userId);
191 + } else {
192 + $relatedBoardsQuery = Board::whereNull('archived_at')->excludeTemplates()->byAccessUser($userId);
193 + }
194 +
195 + $relatedBoards = $relatedBoardsQuery->with('stages')->get();
196 +
197 + // Fetch the stages associated with the boards
198 + $stages = Stage::whereIn('board_id', $relatedBoards->pluck('id'))->where('archived_at', null)->get();
199 +
77 200 return $this->sendSuccess([
78 - 'boards' => $relatedBoards
201 + 'boards' => $relatedBoards,
202 + 'all_stages' => $stages,
79 203 ], 200);
80 204 }
81 -
82 - public function getBoardsList(Request $request)
205 + public function getOnlyBoardsByUser(Request $request)
83 206 {
84 207 try {
85 208 $userId = get_current_user_id();
86 209
87 - if (PermissionManager::isAdmin($userId)) {
88 - $relatedBoardsQuery = Board::query()->where('type', 'to-do');
210 + $searchInput = $request->getSafe('searchInput', 'sanitize_text_field');
211 +
212 +
213 + if(!defined('FLUENT_ROADMAP'))
214 + {
215 + $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->excludeTemplates()->byAccessUser($userId);
89 216 } else {
90 - $currentUser = User::find($userId);
91 - $relatedBoardsQuery = $currentUser->whichBoards()->where('type', 'to-do');
217 + $relatedBoardsQuery = Board::whereNull('archived_at')->excludeTemplates()->byAccessUser($userId);
92 218 }
93 219
94 - $relatedBoards = $relatedBoardsQuery->with('stages')->get();
95 - $stages = Stage::whereIn('board_id', $relatedBoards->pluck('id'))->get();
220 + if (!empty($searchInput)) {
221 + $relatedBoardsQuery = $relatedBoardsQuery->where('title', 'like', '%' . $searchInput . '%');
222 + }
96 223
224 + $relatedBoards = $relatedBoardsQuery->orderBy('created_at', 'DESC')->get();
225 +
97 226 return $this->sendSuccess([
98 - 'boards' => $relatedBoards,
99 - 'all_stages' => $stages,
100 - ], 200);
227 + 'boards' => $relatedBoards
228 + ]);
101 229 } catch (\Exception $e) {
102 - return $this->sendError($e->getMessage(), 404);
230 + return $this->sendError([
231 + 'message' => $e->getMessage()
232 + ]);
103 233 }
104 234 }
105 235
106 236 public function getRecentBoards()
@@ -107,9 +237,12 @@
107 237 {
108 238 $boards = $this->boardService->getRecentBoards();
109 239
110 240 if (!$boards || $boards->isEmpty()) {
111 - $boards = Board::where('type', 'to-do')->byAccessUser(get_current_user_id())
241 + $boards = Board::whereNull('archived_at')
242 + ->excludeTemplates()
243 + ->availableInCurrentInstall()
244 + ->byAccessUser(get_current_user_id())
112 245 ->limit(4)
113 246 ->withCount('completedTasks')
114 247 ->with(['stages', 'users'])
115 248 ->get();
@@ -116,8 +249,9 @@
116 249 }
117 250
118 251 foreach ($boards as $board) {
119 252 $board->users = Helper::sanitizeUserCollections($board->users);
253 + $board->is_pinned = $this->boardService->isPinned($board->id);
120 254 }
121 255
122 256 return [
123 257 'boards' => $boards,
@@ -132,9 +266,9 @@
132 266 $boards = $this->boardService->getBoardsByType($type);
133 267
134 268 return $this->sendSuccess([
135 269 'boards' => $boards,
136 - ], 200);
270 + ]);
137 271 }
138 272
139 273 public function createFirstBoard(Request $request)
140 274 {
@@ -145,11 +279,14 @@
145 279 'currency' => 'nullable|string',
146 280 'crm_contact_id' => 'nullable|numeric',
147 281 ]);
148 282
149 - $installFluentCRM = $request->get('withFluentCRM') == 'yes' ? true : false;
283 + $installFluentCRM = $request->getSafe('withFluentCRM', 'sanitize_text_field') == 'yes' ? true : false;
150 284
151 285 $postStages = $request->get('stages');
286 + if (!is_array($postStages)) {
287 + $postStages = [];
288 + }
152 289 $stageData = array();
153 290 foreach ($postStages as $stage) {
154 291 $temp = $this->stageSanitizeAndValidate($stage, [
155 292 'title' => 'required|string',
@@ -159,9 +296,9 @@
159 296
160 297 $taskData = null;
161 298 if ($request->get('task')) {
162 299 $taskData = $this->taskSanitizeAndValidate($request->get('task'), [
163 - 'title' => 'required|string'
300 + 'title' => 'required|string',
164 301 ]);
165 302 }
166 303
167 304 $board = $this->boardService->createBoard($boardData);
@@ -175,9 +312,9 @@
175 312 $this->taskService->createTask($taskData, $board->id);
176 313 }
177 314
178 315 do_action('fluent_boards/board_created', $board);
179 -
316 +
180 317 if ($installFluentCRM && !defined('FLUENTCRM')) {
181 318 InstallService::install('fluent-crm');
182 319 }
183 320
@@ -186,8 +323,21 @@
186 323 'board' => $board,
187 324 ];
188 325 }
189 326
327 + public function skipOnboarding(Request $request)
328 + {
329 + $onboarding = Meta::where('key', Constant::FBS_ONBOARDING)->first();
330 + if($onboarding && $onboarding->value == 'no'){
331 + $onboarding->value = 'yes' ;
332 + $onboarding->save();
333 + }
334 +
335 + return [
336 + 'message' => __('Onboarding skipped successfully', 'fluent-boards'),
337 + ];
338 + }
339 +
190 340 public function create(Request $request)
191 341 {
192 342 $boardData = $this->boardSanitizeAndValidate($request->get('board'), [
193 343 'title' => 'required|string',
@@ -194,17 +344,46 @@
194 344 'description' => 'nullable',
195 345 'type' => 'required|string',
196 346 'currency' => 'nullable|string',
197 347 'crm_contact_id' => 'nullable|numeric',
348 + 'folder_id' => 'nullable',
198 349 ]);
199 350
200 351 try {
352 + $folderId = $request->getSafe('folder_id', 'intval');
353 + $folderService = new FolderService();
354 + if ($folderId) {
355 + $folderService->assertCanModifyFolder($folderId);
356 + }
357 +
358 + $backgroundData = $this->sanitizeCreateBoardBackground($request->get('background'));
359 + if (!empty($backgroundData)) {
360 + $boardData['background'] = $backgroundData;
361 + }
362 +
363 + $this->validateRequestedLabelPresets($request->get('labels'));
364 +
201 365 $board = $this->boardService->createBoard($boardData);
202 - $this->labelService->createDefaultLabel($board->id);
366 + $this->createBoardLabelsFromRequest($request, $board->id);
367 + $this->addBoardMembersFromRequest($request, $board->id);
203 368 $type = ucfirst($boardData['type']);
369 + $stages = $request->get('stages');
370 + $sanitizedStages = [];
204 371
372 + if (is_array($stages) && !empty($stages)) {
373 + foreach ($stages as $stage) {
374 + $sanitizedStages[] = $this->stageSanitizeAndValidate($stage, [
375 + 'title' => 'required|string',
376 + 'slug' => 'nullable|string',
377 + 'position' => 'nullable|numeric'
378 + ]);
379 + }
380 + }
381 +
205 382 if (isset($boardData['type']) && $boardData['type'] == 'roadmap') {
206 - $this->stageService->createRoadmapStages($board, $request->get('stages'));
383 + $this->stageService->createRoadmapStages($board, $sanitizedStages);
384 + } elseif (!empty($sanitizedStages)) {
385 + $this->stageService->createStages($board, $sanitizedStages);
207 386 } else {
208 387 $this->stageService->createDefaultStages($board);
209 388 }
210 389
@@ -214,37 +393,151 @@
214 393 }
215 394
216 395 do_action('fluent_boards/board_created', $board);
217 396
397 +
398 + if ($folderId) {
399 + $folderService->addBoardToFolder($folderId, [$board->id]);
400 + }
401 +
218 402 $message = __('Board has been created successfully', 'fluent-boards');
219 403
220 - return $this->sendSuccess([
404 + return $this->send([
221 405 'message' => $message,
222 406 'board' => $board,
223 407 ], 201);
224 408 } catch (\Exception $e) {
225 - return $this->sendError($e->getMessage(), 400);
409 + return $this->sendError([
410 + 'message' => $e->getMessage()
411 + ]);
226 412 }
227 413 }
228 414
415 + private function sanitizeCreateBoardBackground($background)
416 + {
417 + if (!is_array($background) || empty($background['id'])) {
418 + return '';
419 + }
420 +
421 + $backgroundId = sanitize_text_field($background['id']);
422 +
423 + // Only accept ids from the curated solid/gradient palettes and always
424 + // persist the canonical value from the constant (never the client-supplied
425 + // color) so arbitrary CSS can't be stored and later rendered into a style.
426 + $allowedBackgrounds = [];
427 + foreach (array_merge(
428 + Constant::BOARD_BACKGROUND_DEFAULT_SOLID_COLORS,
429 + Constant::BOARD_BACKGROUND_DEFAULT_GRADIENT_COLORS
430 + ) as $option) {
431 + if (isset($option['id'], $option['value'])) {
432 + $allowedBackgrounds[$option['id']] = $option['value'];
433 + }
434 + }
435 +
436 + if (!isset($allowedBackgrounds[$backgroundId])) {
437 + return '';
438 + }
439 +
440 + return [
441 + 'id' => $backgroundId,
442 + 'color' => $allowedBackgrounds[$backgroundId],
443 + 'is_image' => false,
444 + 'image_url' => null,
445 + ];
446 + }
447 +
448 + private function createBoardLabelsFromRequest(Request $request, $boardId)
449 + {
450 + $labels = $request->get('labels');
451 +
452 + if (!is_array($labels)) {
453 + $this->labelService->createDefaultLabel($boardId);
454 + return;
455 + }
456 +
457 + foreach ($labels as $label) {
458 + $labelData = Helper::sanitizeLabel((array) $label);
459 +
460 + if (empty($labelData['label']) && empty($labelData['bg_color']) && empty($labelData['color_preset'])) {
461 + continue;
462 + }
463 +
464 + $labelPayload = [
465 + 'label' => $labelData['label'] ?? '',
466 + 'bg_color' => $labelData['bg_color'] ?? '#f3f4f6',
467 + 'color' => $labelData['color'] ?? '#1B2533',
468 + ];
469 +
470 + if (array_key_exists('color_preset', $labelData)) {
471 + $labelPayload['color_preset'] = $labelData['color_preset'];
472 + }
473 +
474 + $this->labelService->createLabel($labelPayload, $boardId);
475 + }
476 + }
477 +
478 + /**
479 + * Reject unsupported label preset ids before creating any board records.
480 + *
481 + * @param mixed $labels
482 + * @return void
483 + * @throws \Exception
484 + */
485 + private function validateRequestedLabelPresets($labels)
486 + {
487 + if (!is_array($labels)) {
488 + return;
489 + }
490 +
491 + foreach ($labels as $label) {
492 + $labelData = Helper::sanitizeLabel((array) $label);
493 + $presetId = $labelData[Constant::LABEL_COLOR_PRESET_SETTING] ?? null;
494 +
495 + if ($presetId === null || $presetId === '') {
496 + continue;
497 + }
498 +
499 + if (!is_string($presetId) || !Constant::getLabelColorPreset($presetId)) {
500 + throw new \Exception(esc_html__('Invalid label color preset', 'fluent-boards'));
501 + }
502 + }
503 + }
504 +
505 + private function addBoardMembersFromRequest(Request $request, $boardId)
506 + {
507 + $memberIds = $request->get('member_ids');
508 +
509 + if (!is_array($memberIds)) {
510 + return;
511 + }
512 +
513 + $memberIds = array_filter(array_unique(array_map('intval', $memberIds)));
514 + $currentUserId = get_current_user_id();
515 +
516 + foreach ($memberIds as $memberId) {
517 + if ($memberId === $currentUserId) {
518 + continue;
519 + }
520 +
521 + $this->boardService->addMembersInBoard($boardId, $memberId);
522 + }
523 + }
524 +
525 + /**
526 + * Get archived stages for a board with optional pagination and archive actor metadata.
527 + */
229 528 public function getArchivedStage(Request $request, $board_id)
230 529 {
231 530 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 - }
531 + $board_id = absint($board_id);
532 + $sanitizedParams = [
533 + 'noPagination' => $request->getSafe('noPagination', 'boolval', false),
534 + 'per_page' => $request->getSafe('per_page', 'intval', 30),
535 + 'page' => $request->getSafe('page', 'intval', 1),
536 + ];
246 537
538 + $stages = $this->stageService->getArchivedStages($sanitizedParams, $board_id);
539 +
247 540 return $this->sendSuccess([
248 541 'stages' => $stages,
249 542 ], 200);
250 543 } catch (\Exception $e) {
@@ -251,17 +544,35 @@
251 544 return $this->sendError($e->getMessage(), 404);
252 545 }
253 546 }
254 547
255 - public function find($board_id)
548 + /** Return board details with a cursor covering changes made during retrieval. */
549 + public function find(Request $request, $board_id)
256 550 {
551 + $syncStartedAt = current_time('mysql');
257 552 $board = Board::findOrFail($board_id);
553 + $board->description = DescriptionMarkdownConverter::normalize($board->description);
554 + $includeArchived = filter_var($request->get('include_archived', false), FILTER_VALIDATE_BOOLEAN);
258 555 $board->background = maybe_unserialize($board->background);
259 556 $board->createdOn = $board->created_at->format('Y-m-d');
260 557
261 - $board->load(['users', 'stages', 'labels', 'owner']);
558 + $board->load(['users', 'labels', 'owner']);
262 559
560 + if ($includeArchived) {
561 + $board->stages = Stage::where('board_id', $board_id)
562 + ->orderBy('position', 'asc')
563 + ->get();
564 + } else {
565 + $board->load('stages');
566 + }
567 +
263 568 if (defined('FLUENT_BOARDS_PRO')){
569 + $customFiledPositionMeta = $board->getMetaByKey('custom_field_positions');
570 + if(!$customFiledPositionMeta) {
571 + (new CustomFieldService())->reIndexCustomFieldPositions($board_id);
572 + $board->updateMeta('custom_field_positions', 'yes');
573 + }
574 +
264 575 $board->load(['customFields']);
265 576 }
266 577
267 578 $this->boardService->updateRecentBoards($board_id);
@@ -267,21 +578,35 @@
267 578 $this->boardService->updateRecentBoards($board_id);
268 579
269 580 $board->labelColor = Constant::TRELLO_COLOR_MAP;
270 581 $board->labelColorText = Constant::TEXT_COLOR_MAP;
582 + $board->labelColorPresets = Constant::LABEL_COLOR_PRESETS;
271 583
272 584 $board->users = Helper::sanitizeUserCollections($board->users);
273 585 $board->owner = Helper::sanitizeUserCollections($board->owner);
274 586
587 + $board->is_pinned = $this->boardService->isPinned($board->id);
588 +
275 589 $board = apply_filters('fluent_boards/board_find', $board);
276 590
277 591 return [
278 - 'board' => $board
592 + 'board' => $board,
593 + 'synced_at' => $syncStartedAt
279 594 ];
280 595 }
281 596
282 597 public function update(Request $request, $board_id)
283 598 {
599 + // Board identity (title/description) is manager-only. This action shares the
600 + // `update` name with CommentController@update under the same policy group, so the
601 + // guard lives here rather than in a SingleBoardPolicy::update() method that would
602 + // also block ordinary members from editing their own comments.
603 + if (!PermissionManager::isBoardManager(absint($board_id))) {
604 + return $this->sendError([
605 + 'message' => __('You do not have permission to edit this board.', 'fluent-boards'),
606 + ], 403);
607 + }
608 +
284 609 $boardData = $this->boardSanitizeAndValidate($request->only(['title', 'description']), [
285 610 'title' => 'required|string',
286 611 'description' => 'nullable|string',
287 612 ]);
@@ -286,8 +611,9 @@
286 611 'description' => 'nullable|string',
287 612 ]);
288 613
289 614 $board = Board::findOrFail($board_id);
615 + $boardData['description'] = DescriptionMarkdownConverter::normalize($boardData['description']);
290 616
291 617 $oldBoard = clone $board;
292 618 $board->fill($boardData);
293 619 $board->save();
@@ -294,21 +620,23 @@
294 620
295 621 do_action('fluent_boards/board_updated', $board, $oldBoard);
296 622
297 623 return [
298 - 'stages' => $board->stages()->get(),
299 624 'message' => __('Board has been updated', 'fluent-boards'),
300 625 'board' => $board,
626 + 'stages' => $board->stages()->get(),
301 627 ];
302 628 }
303 629
304 630 public function archiveStage($board_id, $stage_id)
305 631 {
632 + $board_id = absint($board_id);
633 + $stage_id = absint($stage_id);
634 +
306 635 try {
307 - $stage = Stage::findOrFail($stage_id);
308 - $board = Board::findOrFail($stage->board_id);
636 + $stage = $this->findStageOnBoard($stage_id, $board_id);
309 637
310 - $updatedStage = $this->boardService->archiveStage($board->id, $stage);
638 + $updatedStage = $this->boardService->archiveStage($board_id, $stage);
311 639
312 640 return $this->sendSuccess([
313 641 'updatedStage' => $updatedStage,
314 642 'message' => __('Stage has been archived', 'fluent-boards'),
@@ -319,18 +647,20 @@
319 647 }
320 648
321 649 public function restoreStage($board_id, $stage_id)
322 650 {
651 + $board_id = absint($board_id);
652 + $stage_id = absint($stage_id);
653 +
323 654 try {
324 - $stage = Stage::findOrFail($stage_id);
325 - $board = Board::findOrFail($board_id);
655 + $stage = $this->findStageOnBoard($stage_id, $board_id);
326 656
327 - $updatedStage = $this->boardService->restoreStage($board->id, $stage);
657 + $updatedStage = $this->boardService->restoreStage($board_id, $stage);
328 658
329 659 return $this->sendSuccess([
330 - 'success' => true,
660 + 'success' => true,
331 661 'updatedStage' => $updatedStage,
332 - 'message' => __('Stage has been restored', 'fluent-boards')
662 + 'message' => __('Stage has been restored', 'fluent-boards')
333 663 ], 200);
334 664 } catch (\Exception $e) {
335 665 return $this->sendError($e->getMessage(), 400);
336 666 }
@@ -336,32 +666,22 @@
336 666 }
337 667 }
338 668
339 669
340 - public function changePositionOfStage(Request $request, $board_id)
670 + public function repositionStages(Request $request, $board_id)
341 671 {
342 - $changeData = $this->boardSanitizeAndValidate($request->only(['fromPosition', 'toPosition']), [
343 - 'fromPosition' => 'required',
344 - 'toPosition' => 'required',
345 - ]);
346 -
672 + $incomingList = $request->get('list');
673 + if (!is_array($incomingList)) {
674 + $incomingList = [];
675 + }
676 + $incomingList = array_map('intval', $incomingList);
347 677 try {
348 - $this->boardService->changePositionOfStage($board_id, $changeData);
678 + foreach ($incomingList as $stageId) {
679 + $this->findStageOnBoard($stageId, $board_id);
680 + }
349 681
682 + $this->boardService->repositionStages($board_id, $incomingList);
350 683 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 684 'message' => __('Stages Reordered', 'fluent-boards'),
365 685 'updatedStages' => $this->stageService->getLastOneMinuteUpdatedStages($board_id)
366 686 ], 200);
367 687 } catch (\Exception $e) {
@@ -379,9 +699,9 @@
379 699 public function delete($board_id)
380 700 {
381 701 try {
382 702 if (!PermissionManager::isAdmin()) {
383 - throw new \Exception('You do not have permission to delete this board', 400);
703 + throw new \Exception(esc_html__('You do not have permission to delete this board', 'fluent-boards'), 400);
384 704 }
385 705 $this->boardService->deleteBoard($board_id);
386 706
387 707 return $this->sendSuccess([
@@ -399,9 +719,12 @@
399 719
400 720 public function getActivities(Request $request, $board_id)
401 721 {
402 722 try {
403 - $activities = $this->boardService->getActivities($board_id, $request->all());
723 + $activities = $this->boardService->getActivities($board_id, [
724 + 'per_page' => $request->getSafe('per_page', 'intval', 40),
725 + 'page' => $request->getSafe('page', 'intval', 1),
726 + ]);
404 727 return $this->sendSuccess([
405 728 'activities' => $activities,
406 729 ], 200);
407 730 } catch (\Exception $e) {
@@ -419,8 +742,11 @@
419 742 $boardObjects = Relation::where('object_type', 'board_user')
420 743 ->where('object_id', $board_id)
421 744 ->get()->keyBy('foreign_id');
422 745
746 + $superAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
747 + ->get()->pluck('object_id')->toArray();
748 +
423 749 $userIds = $boardObjects->pluck('foreign_id')->toArray();
424 750
425 751 $coreUsers = [];
426 752 if ($userIds) {
@@ -439,14 +765,17 @@
439 765 }
440 766
441 767 $boardRelation = $boardObjects[$user->ID] ?? null;
442 768
769 +
443 770 $formattedUsers[] = [
444 771 'ID' => $user->ID,
445 772 'display_name' => $name,
446 773 'email' => $user->user_email,
447 774 'photo' => fluent_boards_user_avatar($user->user_email, $name),
448 - 'role' => $boardRelation && $boardRelation->settings['is_admin'] ? 'manager' : 'member'
775 + 'role' => $this->boardUserRole($boardRelation),
776 + 'is_super' => in_array($user->ID, $superAdminIds),
777 + 'is_wpadmin' => $user->has_cap('manage_options')
449 778 ];
450 779 }
451 780
452 781 // order formatted users by display_name
@@ -463,16 +792,24 @@
463 792 return $returnData;
464 793 }
465 794
466 795 /*
467 - * These are the rest of the admin users who are not in the board
796 + * These are the rest of the Fluent Boards and WordPress admins who are not in the board.
468 797 */
469 - $adminUserIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
798 + $fluentBoardAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
470 799 ->whereNotIn('object_id', $userIds)
471 800 ->get()
472 801 ->pluck('object_id')
473 802 ->toArray();
474 803
804 + $wordPressAdminIds = get_users([
805 + 'capability' => 'manage_options',
806 + 'exclude' => $userIds,
807 + 'fields' => 'ID',
808 + ]);
809 +
810 + $adminUserIds = array_values(array_unique(array_map('intval', array_merge($fluentBoardAdminIds, $wordPressAdminIds))));
811 +
475 812 if ($adminUserIds) {
476 813 $adminUsers = get_users([
477 814 'include' => $adminUserIds,
478 815 ]);
@@ -489,9 +826,11 @@
489 826 'ID' => $user->ID,
490 827 'display_name' => $name,
491 828 'email' => $user->user_email,
492 829 'photo' => fluent_boards_user_avatar($user->user_email, $name),
493 - 'role' => 'admin'
830 + 'role' => 'admin',
831 + 'is_super' => in_array($user->ID, $superAdminIds),
832 + 'is_wpadmin' => $user->has_cap('manage_options')
494 833 ];
495 834 }
496 835
497 836 // order formatted users by display_name
@@ -520,20 +859,28 @@
520 859 }
521 860
522 861 public function addMembersInBoard(Request $request, $board_id)
523 862 {
524 - $memberId = $request->getSafe('memberId');
525 - $isAlreadyMember = $this->boardService->isAlreadyMember($board_id, $memberId);
863 + $memberId = $request->getSafe('memberId', 'intval');
864 + $isViewerOnly = $request->getSafe('isViewerOnly', 'sanitize_text_field');
865 + $member = $this->boardService->addMembersInBoard($board_id, $memberId, $isViewerOnly);
526 866
527 - if ($isAlreadyMember) {
867 + if ($member === null) {
528 868 return $this->sendError([
869 + 'message' => __('User not found.', 'fluent-boards'),
870 + ], 404);
871 + }
872 +
873 + if (!$member) {
874 + return $this->sendError([
529 875 'message' => __('User already a member', 'fluent-boards'),
530 - ], 304);
876 + ], 409);
531 877 }
532 - $member = $this->boardService->addMembersInBoard($board_id, $memberId);
533 878
879 +
534 880 return [
535 881 'message' => __('Member added successfully', 'fluent-boards'),
882 + 'is_admin' => PermissionManager::isAdmin($memberId),
536 883 'member' => Helper::sanitizeUserCollections($member)
537 884 ];
538 885 }
539 886
@@ -559,11 +906,11 @@
559 906 }
560 907
561 908 public function searchBoards(Request $request)
562 909 {
563 - $per_page = $request->get('per_page', 10);
564 - $search_input = $request->searchInput . trim('');
565 - $type = $request->type;
910 + $per_page = $request->getSafe('per_page', 'intval', 10);
911 + $search_input = $request->getSafe('searchInput', 'sanitize_text_field', '');
912 + $type = $request->getSafe('type', 'sanitize_text_field', 'to-do');
566 913
567 914 $currentUserId = get_current_user_id();
568 915
569 916 if (PermissionManager::isAdmin($currentUserId)) {
@@ -605,10 +952,13 @@
605 952 * @return
606 953 */
607 954 public function changeStageView($board_id, $stage_id)
608 955 {
956 + $board_id = absint($board_id);
957 + $stage_id = absint($stage_id);
958 +
609 959 try {
610 - $stage = Stage::findOrFail($stage_id);
960 + $stage = $this->findStageOnBoard($stage_id, $board_id);
611 961 $message = __('The stage is made public!', 'fluent-boards');
612 962 $settings = $stage->settings;
613 963
614 964 if (isset($settings['is_public'])) {
@@ -613,9 +963,9 @@
613 963
614 964 if (isset($settings['is_public'])) {
615 965 if ($settings['is_public']) {
616 966 $settings['is_public'] = false;
617 - $message = __('The stage is made admin only!', 'fluent-boards');
967 + $message = __('The stage is made private!', 'fluent-boards');
618 968 } else {
619 969 $settings['is_public'] = true;
620 970 }
621 971 } else {
@@ -634,24 +984,27 @@
634 984 }
635 985
636 986
637 987 /**
638 - * Set board background image or color
988 + * Set or reset board background image/color.
639 989 * @param \FluentBoards\Framework\Http\Request\Request $request
640 990 * @return
641 991 */
642 992 public function setBoardBackground(Request $request, $board_id)
643 993 {
644 - // sanitize and validate image_url
645 - if ($request->image_url) {
994 + $backgroundData = [];
995 + $isResetRequest = $request->getSafe('reset', 'rest_sanitize_boolean');
996 +
997 + if ($isResetRequest) {
998 + $backgroundData = [
999 + 'reset' => true,
1000 + ];
1001 + } elseif ($request->image_url) {
646 1002 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
647 - "id" => 'required',
1003 + 'id' => 'required|integer',
648 1004 'image_url' => 'required|string|url',
649 1005 ]);
650 - }
651 -
652 - // sanitize and validate color
653 - if ($request->color) {
1006 + } elseif ($request->color) {
654 1007 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
655 1008 "id" => 'required',
656 1009 'color' => 'required',
657 1010 ]);
@@ -659,17 +1012,22 @@
659 1012
660 1013 try {
661 1014 if (!$board_id) {
662 1015 $errorMessage = __('Board id is required', 'fluent-boards');
663 - throw new \Exception($errorMessage, 400);
1016 + throw new \Exception(esc_html($errorMessage), 400);
664 1017 }
665 1018
1019 + if (empty($backgroundData)) {
1020 + $errorMessage = __('Background data is required', 'fluent-boards');
1021 + throw new \Exception(esc_html($errorMessage), 400);
1022 + }
1023 +
666 1024 return $this->sendSuccess([
667 1025 'message' => __('Background updated successfully', 'fluent-boards'),
668 1026 'background' => $this->boardService->setBoardBackground($backgroundData, $board_id),
669 1027 ]);
670 1028 } catch (\Exception $e) {
671 - $this->sendError([$e->getMessage(), 400]);
1029 + return $this->sendError($e->getMessage(), 400);
672 1030 }
673 1031 }
674 1032
675 1033
@@ -679,15 +1037,19 @@
679 1037 * @param mixed $stage_slug
680 1038 * @return $availablePositions as an array
681 1039 * @throws \Exception
682 1040 */
683 - public function getStageTaskAvailablePositions($board_id, $stage_id)
1041 + public function getStageTaskAvailablePositions(Request $request, $board_id, $stage_id)
684 1042 {
685 1043 try {
686 1044 if ($board_id && $stage_id) {
687 - $availablePositions = $this->boardService->getStageTaskAvailablePositions($board_id, $stage_id);
1045 + $taskId = $request->getSafe('task_id', 'intval');
1046 + $availablePositions = $this->boardService->getStageTaskAvailablePositions($board_id, $stage_id, $taskId);
688 1047 return $this->sendSuccess([
689 - 'availablePositions' => $availablePositions
1048 + 'availablePositions' => $availablePositions['availablePositions'],
1049 + 'moveTargets' => $availablePositions['moveTargets'],
1050 + 'currentMoveTargetKey' => $availablePositions['currentMoveTargetKey'],
1051 + 'defaultMoveTargetKey' => $availablePositions['defaultMoveTargetKey'],
690 1052 ], 200);
691 1053 } else {
692 1054 $message = '';
693 1055 if (!$board_id) {
@@ -695,12 +1057,12 @@
695 1057 }
696 1058 if (!$stage_id) {
697 1059 $message = 'Stage ';
698 1060 }
699 - throw new \Exception($message . 'is required', 400);
1061 + throw new \Exception(esc_html($message . 'is required'), 400);
700 1062 }
701 1063 } catch (\Exception $e) {
702 - $this->sendError([$e->getMessage(), 400]);
1064 + return $this->sendError($e->getMessage(), 400);
703 1065 }
704 1066 }
705 1067
706 1068 public function getAssociateCrmContacts($board_id)
@@ -709,24 +1071,47 @@
709 1071 $contactAssociatedTasks = Task::with('board')->where('board_id', $board_id)
710 1072 ->whereNotNull('crm_contact_id')
711 1073 ->get();
712 1074
713 - $formattedContacts = Collection::make($contactAssociatedTasks)
714 - ->groupBy('crm_contact_id')
715 - ->map(function ($tasks, $contactId) {
716 - $subscriber = Subscriber::find($contactId);
717 - if (!$subscriber) {
1075 + $tasksByContact = [];
1076 + foreach ($contactAssociatedTasks as $task) {
1077 + $tasksByContact[absint($task->crm_contact_id)][] = $task;
1078 + }
1079 +
1080 + $boardContactIds = Meta::query()
1081 + ->where('object_id', absint($board_id))
1082 + ->where('object_type', Constant::OBJECT_TYPE_BOARD)
1083 + ->whereIn('key', [
1084 + Constant::BOARD_ASSOCIATED_CRM_CONTACT,
1085 + self::LEGACY_BOARD_ASSOCIATED_CRM_CONTACT,
1086 + ])
1087 + ->pluck('value')
1088 + ->toArray();
1089 + $boardContactIds = array_values(array_unique(array_filter(array_map('absint', $boardContactIds))));
1090 +
1091 + $contactIds = array_values(array_unique(array_filter(array_map('absint', array_merge(
1092 + array_keys($tasksByContact),
1093 + $boardContactIds
1094 + )))));
1095 +
1096 + usort($contactIds, function ($firstContactId, $secondContactId) use ($boardContactIds) {
1097 + return (int) in_array($secondContactId, $boardContactIds, true) - (int) in_array($firstContactId, $boardContactIds, true);
1098 + });
1099 +
1100 + $formattedContacts = Collection::make($contactIds)
1101 + ->map(function ($contactId) use ($tasksByContact, $boardContactIds) {
1102 + $contact = Helper::crm_contact($contactId);
1103 + if (!$contact) {
718 1104 return null; // Skip if subscriber not found
719 1105 }
720 1106
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 - ];
1107 + $tasks = $tasksByContact[$contactId] ?? [];
1108 + $contact['name'] = trim(($contact['first_name'] ?? '') . ' ' . ($contact['last_name'] ?? '')) ?: ($contact['full_name'] ?? $contact['email'] ?? '');
1109 + $contact['crm_contact_id'] = $contactId;
1110 + $contact['is_board_contact'] = in_array($contactId, $boardContactIds, true);
1111 + $contact['tasks'] = $tasks;
1112 +
1113 + return $contact;
729 1114 })
730 1115 ->filter()->toArray();
731 1116
732 1117
@@ -747,11 +1132,13 @@
747 1132 'message' => __('Associated Crm Member has been updated', 'fluent-boards'),
748 1133 ], 200);
749 1134 }
750 1135
751 - public function hasDataChanged($board_id)
1136 + public function hasDataChanged(Request $request, $board_id)
752 1137 {
753 - return $this->boardService->hasDataChanged($board_id);
1138 + $includeArchived = filter_var($request->get('include_archived', false), FILTER_VALIDATE_BOOLEAN);
1139 + $since = $request->getSafe('since', 'sanitize_text_field');
1140 + return $this->boardService->hasDataChanged($board_id, $includeArchived, $since);
754 1141 }
755 1142
756 1143 public function createStage(Request $request, $board_id)
757 1144 {
@@ -756,8 +1143,9 @@
756 1143 public function createStage(Request $request, $board_id)
757 1144 {
758 1145 $stageData = $this->stageSanitizeAndValidate($request->all(), [
759 1146 'title' => 'required|string',
1147 + 'position' => 'nullable|numeric'
760 1148 ]);
761 1149
762 1150 $board = Board::find($board_id);
763 1151 $stage = $this->stageService->createStage($stageData, $board_id);
@@ -773,15 +1161,27 @@
773 1161 }
774 1162
775 1163 public function moveAllTasks(Request $request, $board_id)
776 1164 {
777 - $oldStageId = $request->getSafe('oldStageId');
778 - $newStageId = $request->getSafe('newStageId');
1165 + $oldStageId = $request->getSafe('oldStageId', 'intval');
1166 + $newStageId = $request->getSafe('newStageId', 'intval');
779 1167
1168 + if (!$oldStageId || !$newStageId) {
1169 + return $this->sendError(__('Invalid stage IDs provided', 'fluent-boards'), 400);
1170 + }
1171 +
1172 + // Verify stages exist and belong to the board
1173 + $oldStage = Stage::where('id', $oldStageId)->where('board_id', $board_id)->first();
1174 + $newStage = Stage::where('id', $newStageId)->where('board_id', $board_id)->first();
1175 +
1176 + if (!$oldStage || !$newStage) {
1177 + return $this->sendError(__('One or both stages do not exist or do not belong to this board', 'fluent-boards'), 400);
1178 + }
1179 +
780 1180 $updates = $this->stageService->moveAllTasks($oldStageId, $newStageId, $board_id);
781 1181
782 1182 return [
783 - 'message' => __('Tasks has been Moved', 'fluent-boards'),
1183 + 'message' => __('Tasks have been moved', 'fluent-boards'),
784 1184 'updatedTasks' => $updates,
785 1185 ];
786 1186
787 1187 }
@@ -787,52 +1187,103 @@
787 1187 }
788 1188
789 1189 public function archiveAllTasksInStage($board_id, $stage_id)
790 1190 {
791 - $updates = $this->stageService->archiveAllTasksInStage($stage_id);
792 - return [
793 - 'message' => __('Tasks has been archived', 'fluent-boards'),
794 - 'updatedTasks' => $updates,
795 - ];
1191 + $board_id = absint($board_id);
1192 + $stage_id = absint($stage_id);
1193 +
1194 + try {
1195 + $this->findStageOnBoard($stage_id, $board_id);
1196 + $updates = $this->stageService->archiveAllTasksInStage($stage_id, $board_id);
1197 +
1198 + return [
1199 + 'message' => __('Tasks have been archived', 'fluent-boards'),
1200 + 'updatedTasks' => $updates,
1201 + ];
1202 + } catch (\Exception $e) {
1203 + return $this->sendError($e->getMessage(), 400);
1204 + }
796 1205 }
797 1206
798 1207 public function getAssociatedBoards(Request $request, $associated_id)
799 1208 {
800 - $associatedBoards = $this->boardService->getAssociatedBoards($associated_id);
1209 + if (!$this->currentUserCanReadCrmContacts()) {
1210 + return $this->sendError(esc_html__('You do not have permission to view CRM contact boards', 'fluent-boards'), 403);
1211 + }
1212 +
1213 + $associatedId = absint($associated_id);
1214 +
1215 + if (!$associatedId) {
1216 + return $this->sendError(__('Invalid CRM contact', 'fluent-boards'), 400);
1217 + }
1218 +
1219 + $associatedBoards = $this->boardService->getAssociatedBoards($associatedId, get_current_user_id());
1220 +
801 1221 return [
802 1222 'boards' => $associatedBoards,
803 1223 ];
804 1224 }
805 1225
1226 + private function currentUserCanReadCrmContacts()
1227 + {
1228 + $permissionManager = 'FluentCrm\\App\\Services\\PermissionManager';
1229 +
1230 + if (!class_exists($permissionManager)) {
1231 + return false;
1232 + }
1233 +
1234 + return (bool) $permissionManager::currentUserCan('fcrm_read_contacts');
1235 + }
1236 +
1237 + /**
1238 + * Duplicate a board with the selected board data options.
1239 + */
806 1240 public function duplicateBoard(Request $request, $board_id)
807 1241 {
808 1242 $boardData = $this->taskSanitizeAndValidate($request->get('board'), [
809 1243 'title' => 'required|string'
810 1244 ]);
1245 +
1246 + $boardData['source_board_id'] = $board_id;
1247 +
811 1248 $isWithLabels = $request->getSafe('isWithLabels');
812 1249 $isWithTasks = $request->getSafe('isWithTasks');
1250 + $isWithMembers = $request->getSafe('isWithMembers');
1251 + $isWithCustomFields = $request->getSafe('isWithCustomFields', 'sanitize_text_field', 'yes');
1252 + $isWithTemplates = $request->getSafe('isWithTemplates');
813 1253
814 1254 try {
815 1255 if(!PermissionManager::isAdmin()) {
816 1256 $errorMessage = __('You do not have permission to duplicate board', 'fluent-boards');
817 - throw new \Exception($errorMessage, 400);
1257 + throw new \Exception(esc_html($errorMessage), 400);
818 1258 }
819 1259 //create board
820 1260 $newBoard = $this->boardService->copyBoard($boardData);
1261 + $customFieldMap = [];
821 1262
1263 + if ($isWithCustomFields == 'yes') {
1264 + $customFieldMap = $this->boardService->copyCustomFieldsOfBoard($board_id, $newBoard);
1265 + }
1266 +
822 1267 //label copy
1268 + $labelMap = [];
1269 +
823 1270 if ($isWithLabels == 'yes') {
824 - $this->labelService->copyLabelsOfBoard($board_id, $newBoard);
1271 + $labelMap = $this->labelService->copyLabelsOfBoard($board_id, $newBoard);
825 1272 }
826 1273
827 1274 //stage copy
828 - $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id);
1275 + $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id, $isWithTemplates);
829 1276
830 1277 //copy tasks of selected stages
831 1278 if ($isWithTasks == 'yes') {
832 - $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard);
1279 + $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap, $isWithTemplates, $customFieldMap);
833 1280 }
834 1281
1282 + if ($isWithMembers == 'yes') {
1283 + $this->boardService->copyMembersOfBoard($board_id, $newBoard);
1284 + }
1285 +
835 1286 return $this->sendSuccess([
836 1287 'board' => $newBoard,
837 1288 ], 200);
838 1289 } catch (\Exception $e) {
@@ -842,11 +1293,18 @@
842 1293
843 1294 public function importFromBoard(Request $request, $board_id)
844 1295 {
845 1296 $selectedStages = $request->getSafe('selectedStages');
1297 + $position = $request->getSafe('position', 'intval');
846 1298
1299 + // Validate and sanitize selectedStages array
1300 + if (!is_array($selectedStages)) {
1301 + $selectedStages = [$selectedStages];
1302 + }
1303 + $selectedStages = array_filter(array_map('intval', $selectedStages));
1304 +
847 1305 try {
848 - $this->stageService->importStagesFromBoard($board_id, $selectedStages);
1306 + $this->stageService->importStagesFromBoard($board_id, $selectedStages, $position);
849 1307
850 1308 return $this->sendSuccess([
851 1309 'message' => __('Import successfully', 'fluent-boards'),
852 1310 ], 200);
@@ -882,7 +1340,221 @@
882 1340 return [
883 1341 'message' => __('Board has been updated', 'fluent-boards'),
884 1342 'board' => apply_filters('fluent_boards/board_find', $board)
885 1343 ];
1344 + }
1345 +
1346 + public function archiveBoard($board_id)
1347 + {
1348 + try {
1349 + $board = $this->boardService->archiveBoard($board_id);
1350 +
1351 + return [
1352 + 'board' => $board,
1353 + 'message' => __('Board has been archived successfully!', 'fluent-boards')
1354 + ];
1355 + } catch (\Exception $e) {
1356 + return $this->sendError($e->getMessage(), 400);
1357 + }
1358 + }
1359 +
1360 + public function restoreBoard($board_id)
1361 + {
1362 + try {
1363 + $board = $this->boardService->restoreBoard($board_id);
1364 +
1365 + return [
1366 + 'board' => $board,
1367 + 'message' => __('Board has been restored successfully!', 'fluent-boards')
1368 + ];
1369 + } catch (\Exception $e) {
1370 + return $this->sendError($e->getMessage(), 400);
1371 + }
1372 + }
1373 +
1374 + private function boardUserRole($boardRelation)
1375 + {
1376 + return $boardRelation && Arr::get($boardRelation->settings, 'is_admin')
1377 + ? 'manager'
1378 + : ($boardRelation && Arr::has($boardRelation->settings, 'is_viewer_only') && Arr::get($boardRelation->settings, 'is_viewer_only')
1379 + ? 'viewer'
1380 + : 'member');
1381 + }
1382 + public function uploadBoardBackground(Request $request,$board_id)
1383 + {
1384 + $file = Arr::get($request->files(), 'file')->toArray();
1385 + (new \FluentBoards\App\Services\UploadService)->validateFile($file);
1386 +
1387 + $uploadInfo = UploadService::handleFileUpload( $request->files(), $board_id);
1388 +
1389 + $fileData = $uploadInfo[0];
1390 + $initialDataData = [
1391 + 'type' => 'url',
1392 + 'url' => '',
1393 + 'name' => '',
1394 + 'size' => 0,
1395 + ];
1396 +
1397 + $attachData = array_merge($initialDataData, $fileData);
1398 + $UrlMeta = [];
1399 + if($attachData['type'] == 'url') {
1400 + $UrlMeta = RemoteUrlParser::parse($attachData['url']);
1401 + }
1402 + $uid = wp_generate_uuid4();
1403 + $fileUploadedData = new Attachment();
1404 + $fileUploadedData->object_id = $board_id;
1405 + $fileUploadedData->object_type = Constant::BOARD_BACKGROUND_IMAGE;
1406 + $fileUploadedData->attachment_type = $attachData['type'];
1407 + $fileUploadedData->title = (new TaskService())->setTitle($attachData['type'], $attachData['name'], $UrlMeta);
1408 + $fileUploadedData->file_path = $attachData['type'] != 'url' ? $attachData['file'] : null;
1409 + $fileUploadedData->full_url = esc_url($attachData['url']);
1410 + $fileUploadedData->file_size = $attachData['size'];
1411 + $fileUploadedData->settings = $attachData['type'] == 'url' ? [
1412 + 'meta' => $UrlMeta
1413 + ] : '';
1414 + $fileUploadedData->driver = 'local';
1415 + $fileUploadedData->file_hash = md5($uid . wp_rand(0, 1000));
1416 + $fileUploadedData->save();
1417 + if(!!defined('FLUENT_BOARDS_PRO_VERSION')) {
1418 + $mediaData = (new AttachmentService())->processMediaData($fileData, $file);
1419 + $fileUploadedData['driver'] = $mediaData['driver'];
1420 + $fileUploadedData['file_path'] = $mediaData['file_path'];
1421 + $fileUploadedData['full_url'] = $mediaData['full_url'];
1422 + $fileUploadedData->save();
1423 + }
1424 +
1425 + $board = Board::find($board_id);
1426 + $oldBackground = $board->background;
1427 + $publicUrl = (new CommentService())->createPublicUrl($fileUploadedData, $board_id);
1428 + $background = [
1429 + 'color' => null,
1430 + 'id' => $fileUploadedData->id,
1431 + 'image_url' => $publicUrl,
1432 + 'is_image' => true,
1433 + ];
1434 + $board->background = $background;
1435 + $board->save();
1436 + do_action('fluent_boards/board_background_updated', $board_id, $oldBackground);
1437 +
1438 + return $this->sendSuccess([
1439 + 'message' => __('Background updated successfully', 'fluent-boards'),
1440 + 'background' => $board->background,
1441 + ]);
1442 + }
1443 +
1444 + public function getPinnedBoards()
1445 + {
1446 + $pinnedBoards = $this->boardService->getPinnedBoards();
1447 +
1448 + return $this->sendSuccess([
1449 + 'pinnedBoards' => $pinnedBoards,
1450 + ], 200);
1451 + }
1452 +
1453 + public function pinBoard($boardId)
1454 + {
1455 + $this->boardService->pinBoard($boardId);
1456 +
1457 + return $this->sendSuccess([
1458 + 'message' => __('The Board has been pinned', 'fluent-boards'),
1459 + ], 200);
1460 + }
1461 +
1462 + public function unpinBoard($boardId)
1463 + {
1464 + $remove = $this->boardService->unpinBoard($boardId);
1465 +
1466 + if (!$remove) {
1467 + return $this->sendError([
1468 + 'message' => __('Board is not pinned', 'fluent-boards'),
1469 + ], 400);
1470 + }
1471 +
1472 + return $this->sendSuccess([
1473 + 'message' => __('Board is removed from pinned boards', 'fluent-boards'),
1474 + ], 200);
1475 + }
1476 +
1477 + public function getBoardFolder($board_id)
1478 + {
1479 + try {
1480 + $folder = $this->boardService->getBoardFolder($board_id);
1481 + return $this->sendSuccess([
1482 + 'folder' => $folder,
1483 + ], 200);
1484 + } catch (\Exception $e) {
1485 + return $this->sendError($e->getMessage(), 400);
1486 + }
1487 + }
1488 +
1489 + public function getBoardMenuItems($board_id)
1490 + {
1491 + try {
1492 + $menuItems = (new BoardMenuHandler())->getMenuItems($board_id);
1493 +
1494 + return $this->sendSuccess([
1495 + 'menu_items' => $menuItems
1496 + ], 200);
1497 + } catch (\Exception $e) {
1498 + return $this->sendError($e->getMessage(), 500);
1499 + }
1500 + }
1501 +
1502 + public function getPublicAccessSettings($board_id)
1503 + {
1504 + $board_id = absint($board_id);
1505 + $board = Board::findOrFail($board_id);
1506 +
1507 + $enabled = (bool) $board->getMetaByKey('public_access_enabled');
1508 + $shortcode = $enabled ? '[fluent_board_public id="' . $board_id . '"]' : '';
1509 +
1510 + return $this->sendSuccess([
1511 + 'enabled' => $enabled,
1512 + 'shortcode' => $shortcode,
1513 + ], 200);
1514 + }
1515 +
1516 + public function togglePublicAccess(Request $request, $board_id)
1517 + {
1518 + $board_id = absint($board_id);
1519 + $board = Board::findOrFail($board_id);
1520 +
1521 + $enabled = filter_var(
1522 + $request->getSafe('enabled', 'sanitize_text_field', false),
1523 + FILTER_VALIDATE_BOOLEAN
1524 + );
1525 +
1526 + $board->updateMeta('public_access_enabled', $enabled ? '1' : '');
1527 +
1528 + $shortcode = $enabled ? '[fluent_board_public id="' . $board_id . '"]' : '';
1529 +
1530 + return $this->sendSuccess([
1531 + 'message' => $enabled
1532 + ? __('Public access has been enabled', 'fluent-boards')
1533 + : __('Public access has been disabled', 'fluent-boards'),
1534 + 'enabled' => $enabled,
1535 + 'shortcode' => $shortcode,
1536 + ], 200);
1537 + }
1538 +
1539 + /**
1540 + * Resolve a stage only when it belongs to the requested board.
1541 + *
1542 + * @param int $stageId
1543 + * @param int $boardId
1544 + * @return Stage
1545 + * @throws \Exception
1546 + */
1547 + private function findStageOnBoard($stageId, $boardId)
1548 + {
1549 + $stage = Stage::where('id', absint($stageId))
1550 + ->where('board_id', absint($boardId))
1551 + ->first();
1552 +
1553 + if (!$stage) {
1554 + throw new \Exception(esc_html__('Stage not found', 'fluent-boards'));
1555 + }
1556 +
1557 + return $stage;
886 1558 }
887 1559
888 1560 }