PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.2.1
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.2.1
2.2.1 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 All 44 releases
← All changes | app/Http/Controllers/BoardController.php +728 -136 1.35 → 2.2.1 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,27 +55,38 @@
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 77 $option = $request->getSafe('option', 'sanitize_text_field');
78 + $folderId = $request->getSafe('fid', 'intval'); // Get folder ID from request
58 79
59 - if(!defined('FLUENT_ROADMAP'))
60 - {
61 - if($option == 'archived') {
80 + // Initialize the query based on archive status
81 + if (!defined('FLUENT_ROADMAP')) {
82 + if ($option == 'archived') {
62 83 $relatedBoardsQuery = Board::whereNotNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
63 84 } else {
64 85 $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
65 86 }
66 87 } else {
67 - if($option == 'archived') {
88 + if ($option == 'archived') {
68 89 $relatedBoardsQuery = Board::whereNotNull('archived_at')->byAccessUser($userId);
69 90 } else {
70 91 $relatedBoardsQuery = Board::whereNull('archived_at')->byAccessUser($userId);
71 92 }
@@ -70,8 +91,30 @@
70 91 $relatedBoardsQuery = Board::whereNull('archived_at')->byAccessUser($userId);
71 92 }
72 93 }
73 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
74 117 if (!empty($searchInput)) {
75 118 $relatedBoardsQuery = $relatedBoardsQuery->where('title', 'like', '%' . $searchInput . '%');
76 119 }
77 120
@@ -76,21 +119,63 @@
76 119 }
77 120
78 121 $relatedBoards = $relatedBoardsQuery->orderBy($order, $orderBy)
79 122 ->withCount('completedTasks')
80 - ->with('stages', 'users')
123 + ->with('stages', 'users', 'boardMeta', 'viewerMembership')
81 124 ->paginate($per_page);
82 125
83 126 foreach ($relatedBoards as $relatedBoard) {
127 + $relatedBoard->description = DescriptionMarkdownConverter::normalize($relatedBoard->description);
84 128 $relatedBoard->users = Helper::sanitizeUserCollections($relatedBoard->users);
129 + $relatedBoard->is_pinned = in_array((int) $relatedBoard->id, $pinnedIds, true);
85 130 }
86 131
87 - return $this->sendSuccess([
88 - 'boards' => $relatedBoards
89 - ], 200);
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);
90 146 }
91 147
92 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 + /**
93 178 * Get the list of boards and their associated stages for the current user.
94 179 *
95 180 * @param \FluentBoards\Framework\Http\Request\Request $request
96 181 * @return \WP_REST_Response
@@ -96,30 +181,56 @@
96 181 * @return \WP_REST_Response
97 182 */
98 183 public function getBoardsList(Request $request)
99 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 +
200 + return $this->sendSuccess([
201 + 'boards' => $relatedBoards,
202 + 'all_stages' => $stages,
203 + ], 200);
204 + }
205 + public function getOnlyBoardsByUser(Request $request)
206 + {
100 207 try {
101 208 $userId = get_current_user_id();
102 209
103 - // Query to fetch boards that are not archived and accessible by the user
104 - // Check if the FLUENT_ROADMAP constant is defined
105 - if (!defined('FLUENT_ROADMAP')) {
106 - $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
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);
107 216 } else {
108 - $relatedBoardsQuery = Board::whereNull('archived_at')->byAccessUser($userId);
217 + $relatedBoardsQuery = Board::whereNull('archived_at')->excludeTemplates()->byAccessUser($userId);
109 218 }
110 219
111 - $relatedBoards = $relatedBoardsQuery->with('stages')->get();
220 + if (!empty($searchInput)) {
221 + $relatedBoardsQuery = $relatedBoardsQuery->where('title', 'like', '%' . $searchInput . '%');
222 + }
112 223
113 - // Fetch the stages associated with the boards
114 - $stages = Stage::whereIn('board_id', $relatedBoards->pluck('id'))->where('archived_at', null)->get();
224 + $relatedBoards = $relatedBoardsQuery->orderBy('created_at', 'DESC')->get();
115 225
116 226 return $this->sendSuccess([
117 - 'boards' => $relatedBoards,
118 - 'all_stages' => $stages,
119 - ], 200);
227 + 'boards' => $relatedBoards
228 + ]);
120 229 } catch (\Exception $e) {
121 - return $this->sendError($e->getMessage(), 404);
230 + return $this->sendError([
231 + 'message' => $e->getMessage()
232 + ]);
122 233 }
123 234 }
124 235
125 236 public function getRecentBoards()
@@ -126,9 +237,12 @@
126 237 {
127 238 $boards = $this->boardService->getRecentBoards();
128 239
129 240 if (!$boards || $boards->isEmpty()) {
130 - $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())
131 245 ->limit(4)
132 246 ->withCount('completedTasks')
133 247 ->with(['stages', 'users'])
134 248 ->get();
@@ -135,8 +249,9 @@
135 249 }
136 250
137 251 foreach ($boards as $board) {
138 252 $board->users = Helper::sanitizeUserCollections($board->users);
253 + $board->is_pinned = $this->boardService->isPinned($board->id);
139 254 }
140 255
141 256 return [
142 257 'boards' => $boards,
@@ -151,9 +266,9 @@
151 266 $boards = $this->boardService->getBoardsByType($type);
152 267
153 268 return $this->sendSuccess([
154 269 'boards' => $boards,
155 - ], 200);
270 + ]);
156 271 }
157 272
158 273 public function createFirstBoard(Request $request)
159 274 {
@@ -164,11 +279,14 @@
164 279 'currency' => 'nullable|string',
165 280 'crm_contact_id' => 'nullable|numeric',
166 281 ]);
167 282
168 - $installFluentCRM = $request->get('withFluentCRM') == 'yes' ? true : false;
283 + $installFluentCRM = $request->getSafe('withFluentCRM', 'sanitize_text_field') == 'yes' ? true : false;
169 284
170 285 $postStages = $request->get('stages');
286 + if (!is_array($postStages)) {
287 + $postStages = [];
288 + }
171 289 $stageData = array();
172 290 foreach ($postStages as $stage) {
173 291 $temp = $this->stageSanitizeAndValidate($stage, [
174 292 'title' => 'required|string',
@@ -178,9 +296,9 @@
178 296
179 297 $taskData = null;
180 298 if ($request->get('task')) {
181 299 $taskData = $this->taskSanitizeAndValidate($request->get('task'), [
182 - 'title' => 'required|string'
300 + 'title' => 'required|string',
183 301 ]);
184 302 }
185 303
186 304 $board = $this->boardService->createBoard($boardData);
@@ -194,9 +312,9 @@
194 312 $this->taskService->createTask($taskData, $board->id);
195 313 }
196 314
197 315 do_action('fluent_boards/board_created', $board);
198 -
316 +
199 317 if ($installFluentCRM && !defined('FLUENTCRM')) {
200 318 InstallService::install('fluent-crm');
201 319 }
202 320
@@ -226,17 +344,46 @@
226 344 'description' => 'nullable',
227 345 'type' => 'required|string',
228 346 'currency' => 'nullable|string',
229 347 'crm_contact_id' => 'nullable|numeric',
348 + 'folder_id' => 'nullable',
230 349 ]);
231 350
232 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 +
233 365 $board = $this->boardService->createBoard($boardData);
234 - $this->labelService->createDefaultLabel($board->id);
366 + $this->createBoardLabelsFromRequest($request, $board->id);
367 + $this->addBoardMembersFromRequest($request, $board->id);
235 368 $type = ucfirst($boardData['type']);
369 + $stages = $request->get('stages');
370 + $sanitizedStages = [];
236 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 +
237 382 if (isset($boardData['type']) && $boardData['type'] == 'roadmap') {
238 - $this->stageService->createRoadmapStages($board, $request->get('stages'));
383 + $this->stageService->createRoadmapStages($board, $sanitizedStages);
384 + } elseif (!empty($sanitizedStages)) {
385 + $this->stageService->createStages($board, $sanitizedStages);
239 386 } else {
240 387 $this->stageService->createDefaultStages($board);
241 388 }
242 389
@@ -246,37 +393,151 @@
246 393 }
247 394
248 395 do_action('fluent_boards/board_created', $board);
249 396
397 +
398 + if ($folderId) {
399 + $folderService->addBoardToFolder($folderId, [$board->id]);
400 + }
401 +
250 402 $message = __('Board has been created successfully', 'fluent-boards');
251 403
252 - return $this->sendSuccess([
404 + return $this->send([
253 405 'message' => $message,
254 406 'board' => $board,
255 407 ], 201);
256 408 } catch (\Exception $e) {
257 - return $this->sendError($e->getMessage(), 400);
409 + return $this->sendError([
410 + 'message' => $e->getMessage()
411 + ]);
258 412 }
259 413 }
260 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 + */
261 528 public function getArchivedStage(Request $request, $board_id)
262 529 {
263 530 try {
264 - $pagination = $request->noPagination ? true : false;
265 - $per_page = isset($data['per_page']) ? $data['per_page'] : 30;
266 - $page = isset($data['page']) ? $data['page'] : 1;
267 - if ($pagination) {
268 - $stages = Stage::where('board_id', $board_id)
269 - ->whereNotNull('archived_at')
270 - ->orderBy('created_at', 'DESC')
271 - ->get();
272 - } else {
273 - $stages = Stage::where('board_id', $board_id)
274 - ->whereNotNull('archived_at')
275 - ->orderBy('created_at', 'DESC')
276 - ->paginate($per_page, ['*'], 'page', $page);
277 - }
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 + ];
278 537
538 + $stages = $this->stageService->getArchivedStages($sanitizedParams, $board_id);
539 +
279 540 return $this->sendSuccess([
280 541 'stages' => $stages,
281 542 ], 200);
282 543 } catch (\Exception $e) {
@@ -283,17 +544,35 @@
283 544 return $this->sendError($e->getMessage(), 404);
284 545 }
285 546 }
286 547
287 - 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)
288 550 {
551 + $syncStartedAt = current_time('mysql');
289 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);
290 555 $board->background = maybe_unserialize($board->background);
291 556 $board->createdOn = $board->created_at->format('Y-m-d');
292 557
293 - $board->load(['users', 'stages', 'labels', 'owner']);
558 + $board->load(['users', 'labels', 'owner']);
294 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 +
295 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 +
296 575 $board->load(['customFields']);
297 576 }
298 577
299 578 $this->boardService->updateRecentBoards($board_id);
@@ -299,21 +578,35 @@
299 578 $this->boardService->updateRecentBoards($board_id);
300 579
301 580 $board->labelColor = Constant::TRELLO_COLOR_MAP;
302 581 $board->labelColorText = Constant::TEXT_COLOR_MAP;
582 + $board->labelColorPresets = Constant::LABEL_COLOR_PRESETS;
303 583
304 584 $board->users = Helper::sanitizeUserCollections($board->users);
305 585 $board->owner = Helper::sanitizeUserCollections($board->owner);
306 586
587 + $board->is_pinned = $this->boardService->isPinned($board->id);
588 +
307 589 $board = apply_filters('fluent_boards/board_find', $board);
308 590
309 591 return [
310 - 'board' => $board
592 + 'board' => $board,
593 + 'synced_at' => $syncStartedAt
311 594 ];
312 595 }
313 596
314 597 public function update(Request $request, $board_id)
315 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 +
316 609 $boardData = $this->boardSanitizeAndValidate($request->only(['title', 'description']), [
317 610 'title' => 'required|string',
318 611 'description' => 'nullable|string',
319 612 ]);
@@ -318,8 +611,9 @@
318 611 'description' => 'nullable|string',
319 612 ]);
320 613
321 614 $board = Board::findOrFail($board_id);
615 + $boardData['description'] = DescriptionMarkdownConverter::normalize($boardData['description']);
322 616
323 617 $oldBoard = clone $board;
324 618 $board->fill($boardData);
325 619 $board->save();
@@ -326,21 +620,23 @@
326 620
327 621 do_action('fluent_boards/board_updated', $board, $oldBoard);
328 622
329 623 return [
330 - 'stages' => $board->stages()->get(),
331 624 'message' => __('Board has been updated', 'fluent-boards'),
332 625 'board' => $board,
626 + 'stages' => $board->stages()->get(),
333 627 ];
334 628 }
335 629
336 630 public function archiveStage($board_id, $stage_id)
337 631 {
632 + $board_id = absint($board_id);
633 + $stage_id = absint($stage_id);
634 +
338 635 try {
339 - $stage = Stage::findOrFail($stage_id);
340 - $board = Board::findOrFail($stage->board_id);
636 + $stage = $this->findStageOnBoard($stage_id, $board_id);
341 637
342 - $updatedStage = $this->boardService->archiveStage($board->id, $stage);
638 + $updatedStage = $this->boardService->archiveStage($board_id, $stage);
343 639
344 640 return $this->sendSuccess([
345 641 'updatedStage' => $updatedStage,
346 642 'message' => __('Stage has been archived', 'fluent-boards'),
@@ -351,18 +647,20 @@
351 647 }
352 648
353 649 public function restoreStage($board_id, $stage_id)
354 650 {
651 + $board_id = absint($board_id);
652 + $stage_id = absint($stage_id);
653 +
355 654 try {
356 - $stage = Stage::findOrFail($stage_id);
357 - $board = Board::findOrFail($board_id);
655 + $stage = $this->findStageOnBoard($stage_id, $board_id);
358 656
359 - $updatedStage = $this->boardService->restoreStage($board->id, $stage);
657 + $updatedStage = $this->boardService->restoreStage($board_id, $stage);
360 658
361 659 return $this->sendSuccess([
362 - 'success' => true,
660 + 'success' => true,
363 661 'updatedStage' => $updatedStage,
364 - 'message' => __('Stage has been restored', 'fluent-boards')
662 + 'message' => __('Stage has been restored', 'fluent-boards')
365 663 ], 200);
366 664 } catch (\Exception $e) {
367 665 return $this->sendError($e->getMessage(), 400);
368 666 }
@@ -368,32 +666,22 @@
368 666 }
369 667 }
370 668
371 669
372 - public function changePositionOfStage(Request $request, $board_id)
670 + public function repositionStages(Request $request, $board_id)
373 671 {
374 - $changeData = $this->boardSanitizeAndValidate($request->only(['fromPosition', 'toPosition']), [
375 - 'fromPosition' => 'required',
376 - 'toPosition' => 'required',
377 - ]);
378 -
672 + $incomingList = $request->get('list');
673 + if (!is_array($incomingList)) {
674 + $incomingList = [];
675 + }
676 + $incomingList = array_map('intval', $incomingList);
379 677 try {
380 - $this->boardService->changePositionOfStage($board_id, $changeData);
678 + foreach ($incomingList as $stageId) {
679 + $this->findStageOnBoard($stageId, $board_id);
680 + }
381 681
682 + $this->boardService->repositionStages($board_id, $incomingList);
382 683 return $this->sendSuccess([
383 - 'message' => __('Board stage has been updated', 'fluent-boards')
384 - ], 200);
385 - } catch (\Exception $e) {
386 - return $this->sendError($e->getMessage(), 400);
387 - }
388 - }
389 -
390 - public function rePositionStages(Request $request, $board_id)
391 - {
392 - $incomingList = $request->get('list');
393 - try {
394 - $this->boardService->rePositionStages($board_id, $incomingList);
395 - return $this->sendSuccess([
396 684 'message' => __('Stages Reordered', 'fluent-boards'),
397 685 'updatedStages' => $this->stageService->getLastOneMinuteUpdatedStages($board_id)
398 686 ], 200);
399 687 } catch (\Exception $e) {
@@ -411,9 +699,9 @@
411 699 public function delete($board_id)
412 700 {
413 701 try {
414 702 if (!PermissionManager::isAdmin()) {
415 - 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);
416 704 }
417 705 $this->boardService->deleteBoard($board_id);
418 706
419 707 return $this->sendSuccess([
@@ -431,9 +719,12 @@
431 719
432 720 public function getActivities(Request $request, $board_id)
433 721 {
434 722 try {
435 - $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 + ]);
436 727 return $this->sendSuccess([
437 728 'activities' => $activities,
438 729 ], 200);
439 730 } catch (\Exception $e) {
@@ -501,16 +792,24 @@
501 792 return $returnData;
502 793 }
503 794
504 795 /*
505 - * 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.
506 797 */
507 - $adminUserIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
798 + $fluentBoardAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
508 799 ->whereNotIn('object_id', $userIds)
509 800 ->get()
510 801 ->pluck('object_id')
511 802 ->toArray();
512 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 +
513 812 if ($adminUserIds) {
514 813 $adminUsers = get_users([
515 814 'include' => $adminUserIds,
516 815 ]);
@@ -560,21 +859,28 @@
560 859 }
561 860
562 861 public function addMembersInBoard(Request $request, $board_id)
563 862 {
564 - $memberId = $request->getSafe('memberId');
565 - $isViewerOnly = $request->getSafe('isViewerOnly');
566 - $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);
567 866
568 - if ($isAlreadyMember) {
867 + if ($member === null) {
569 868 return $this->sendError([
869 + 'message' => __('User not found.', 'fluent-boards'),
870 + ], 404);
871 + }
872 +
873 + if (!$member) {
874 + return $this->sendError([
570 875 'message' => __('User already a member', 'fluent-boards'),
571 - ], 304);
876 + ], 409);
572 877 }
573 - $member = $this->boardService->addMembersInBoard($board_id, $memberId, $isViewerOnly);
574 878
879 +
575 880 return [
576 881 'message' => __('Member added successfully', 'fluent-boards'),
882 + 'is_admin' => PermissionManager::isAdmin($memberId),
577 883 'member' => Helper::sanitizeUserCollections($member)
578 884 ];
579 885 }
580 886
@@ -600,11 +906,11 @@
600 906 }
601 907
602 908 public function searchBoards(Request $request)
603 909 {
604 - $per_page = $request->get('per_page', 10);
605 - $search_input = $request->searchInput . trim('');
606 - $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');
607 913
608 914 $currentUserId = get_current_user_id();
609 915
610 916 if (PermissionManager::isAdmin($currentUserId)) {
@@ -646,10 +952,13 @@
646 952 * @return
647 953 */
648 954 public function changeStageView($board_id, $stage_id)
649 955 {
956 + $board_id = absint($board_id);
957 + $stage_id = absint($stage_id);
958 +
650 959 try {
651 - $stage = Stage::findOrFail($stage_id);
960 + $stage = $this->findStageOnBoard($stage_id, $board_id);
652 961 $message = __('The stage is made public!', 'fluent-boards');
653 962 $settings = $stage->settings;
654 963
655 964 if (isset($settings['is_public'])) {
@@ -654,9 +963,9 @@
654 963
655 964 if (isset($settings['is_public'])) {
656 965 if ($settings['is_public']) {
657 966 $settings['is_public'] = false;
658 - $message = __('The stage is made admin only!', 'fluent-boards');
967 + $message = __('The stage is made private!', 'fluent-boards');
659 968 } else {
660 969 $settings['is_public'] = true;
661 970 }
662 971 } else {
@@ -675,24 +984,27 @@
675 984 }
676 985
677 986
678 987 /**
679 - * Set board background image or color
988 + * Set or reset board background image/color.
680 989 * @param \FluentBoards\Framework\Http\Request\Request $request
681 990 * @return
682 991 */
683 992 public function setBoardBackground(Request $request, $board_id)
684 993 {
685 - // sanitize and validate image_url
686 - 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) {
687 1002 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
688 - "id" => 'required',
1003 + 'id' => 'required|integer',
689 1004 'image_url' => 'required|string|url',
690 1005 ]);
691 - }
692 -
693 - // sanitize and validate color
694 - if ($request->color) {
1006 + } elseif ($request->color) {
695 1007 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
696 1008 "id" => 'required',
697 1009 'color' => 'required',
698 1010 ]);
@@ -700,17 +1012,22 @@
700 1012
701 1013 try {
702 1014 if (!$board_id) {
703 1015 $errorMessage = __('Board id is required', 'fluent-boards');
704 - throw new \Exception($errorMessage, 400);
1016 + throw new \Exception(esc_html($errorMessage), 400);
705 1017 }
706 1018
1019 + if (empty($backgroundData)) {
1020 + $errorMessage = __('Background data is required', 'fluent-boards');
1021 + throw new \Exception(esc_html($errorMessage), 400);
1022 + }
1023 +
707 1024 return $this->sendSuccess([
708 1025 'message' => __('Background updated successfully', 'fluent-boards'),
709 1026 'background' => $this->boardService->setBoardBackground($backgroundData, $board_id),
710 1027 ]);
711 1028 } catch (\Exception $e) {
712 - $this->sendError([$e->getMessage(), 400]);
1029 + return $this->sendError($e->getMessage(), 400);
713 1030 }
714 1031 }
715 1032
716 1033
@@ -720,15 +1037,19 @@
720 1037 * @param mixed $stage_slug
721 1038 * @return $availablePositions as an array
722 1039 * @throws \Exception
723 1040 */
724 - public function getStageTaskAvailablePositions($board_id, $stage_id)
1041 + public function getStageTaskAvailablePositions(Request $request, $board_id, $stage_id)
725 1042 {
726 1043 try {
727 1044 if ($board_id && $stage_id) {
728 - $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);
729 1047 return $this->sendSuccess([
730 - 'availablePositions' => $availablePositions
1048 + 'availablePositions' => $availablePositions['availablePositions'],
1049 + 'moveTargets' => $availablePositions['moveTargets'],
1050 + 'currentMoveTargetKey' => $availablePositions['currentMoveTargetKey'],
1051 + 'defaultMoveTargetKey' => $availablePositions['defaultMoveTargetKey'],
731 1052 ], 200);
732 1053 } else {
733 1054 $message = '';
734 1055 if (!$board_id) {
@@ -736,12 +1057,12 @@
736 1057 }
737 1058 if (!$stage_id) {
738 1059 $message = 'Stage ';
739 1060 }
740 - throw new \Exception($message . 'is required', 400);
1061 + throw new \Exception(esc_html($message . 'is required'), 400);
741 1062 }
742 1063 } catch (\Exception $e) {
743 - $this->sendError([$e->getMessage(), 400]);
1064 + return $this->sendError($e->getMessage(), 400);
744 1065 }
745 1066 }
746 1067
747 1068 public function getAssociateCrmContacts($board_id)
@@ -750,24 +1071,47 @@
750 1071 $contactAssociatedTasks = Task::with('board')->where('board_id', $board_id)
751 1072 ->whereNotNull('crm_contact_id')
752 1073 ->get();
753 1074
754 - $formattedContacts = Collection::make($contactAssociatedTasks)
755 - ->groupBy('crm_contact_id')
756 - ->map(function ($tasks, $contactId) {
757 - $subscriber = Subscriber::find($contactId);
758 - 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) {
759 1104 return null; // Skip if subscriber not found
760 1105 }
761 1106
762 - return [
763 - 'name' => $subscriber->first_name . ' ' . $subscriber->last_name,
764 - 'photo' => $subscriber->photo,
765 - 'email' => $subscriber->email,
766 - 'crm_contact_id' => $contactId,
767 - 'id' => $contactId,
768 - 'tasks' => $tasks,
769 - ];
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;
770 1114 })
771 1115 ->filter()->toArray();
772 1116
773 1117
@@ -788,11 +1132,13 @@
788 1132 'message' => __('Associated Crm Member has been updated', 'fluent-boards'),
789 1133 ], 200);
790 1134 }
791 1135
792 - public function hasDataChanged($board_id)
1136 + public function hasDataChanged(Request $request, $board_id)
793 1137 {
794 - 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);
795 1141 }
796 1142
797 1143 public function createStage(Request $request, $board_id)
798 1144 {
@@ -797,8 +1143,9 @@
797 1143 public function createStage(Request $request, $board_id)
798 1144 {
799 1145 $stageData = $this->stageSanitizeAndValidate($request->all(), [
800 1146 'title' => 'required|string',
1147 + 'position' => 'nullable|numeric'
801 1148 ]);
802 1149
803 1150 $board = Board::find($board_id);
804 1151 $stage = $this->stageService->createStage($stageData, $board_id);
@@ -814,15 +1161,27 @@
814 1161 }
815 1162
816 1163 public function moveAllTasks(Request $request, $board_id)
817 1164 {
818 - $oldStageId = $request->getSafe('oldStageId');
819 - $newStageId = $request->getSafe('newStageId');
1165 + $oldStageId = $request->getSafe('oldStageId', 'intval');
1166 + $newStageId = $request->getSafe('newStageId', 'intval');
820 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 +
821 1180 $updates = $this->stageService->moveAllTasks($oldStageId, $newStageId, $board_id);
822 1181
823 1182 return [
824 - 'message' => __('Tasks has been Moved', 'fluent-boards'),
1183 + 'message' => __('Tasks have been moved', 'fluent-boards'),
825 1184 'updatedTasks' => $updates,
826 1185 ];
827 1186
828 1187 }
@@ -828,39 +1187,84 @@
828 1187 }
829 1188
830 1189 public function archiveAllTasksInStage($board_id, $stage_id)
831 1190 {
832 - $updates = $this->stageService->archiveAllTasksInStage($stage_id);
833 - return [
834 - 'message' => __('Tasks has been archived', 'fluent-boards'),
835 - 'updatedTasks' => $updates,
836 - ];
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 + }
837 1205 }
838 1206
839 1207 public function getAssociatedBoards(Request $request, $associated_id)
840 1208 {
841 - $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 +
842 1221 return [
843 1222 'boards' => $associatedBoards,
844 1223 ];
845 1224 }
846 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 + */
847 1240 public function duplicateBoard(Request $request, $board_id)
848 1241 {
849 1242 $boardData = $this->taskSanitizeAndValidate($request->get('board'), [
850 1243 'title' => 'required|string'
851 1244 ]);
1245 +
1246 + $boardData['source_board_id'] = $board_id;
1247 +
852 1248 $isWithLabels = $request->getSafe('isWithLabels');
853 1249 $isWithTasks = $request->getSafe('isWithTasks');
1250 + $isWithMembers = $request->getSafe('isWithMembers');
1251 + $isWithCustomFields = $request->getSafe('isWithCustomFields', 'sanitize_text_field', 'yes');
1252 + $isWithTemplates = $request->getSafe('isWithTemplates');
854 1253
855 1254 try {
856 1255 if(!PermissionManager::isAdmin()) {
857 1256 $errorMessage = __('You do not have permission to duplicate board', 'fluent-boards');
858 - throw new \Exception($errorMessage, 400);
1257 + throw new \Exception(esc_html($errorMessage), 400);
859 1258 }
860 1259 //create board
861 1260 $newBoard = $this->boardService->copyBoard($boardData);
1261 + $customFieldMap = [];
862 1262
1263 + if ($isWithCustomFields == 'yes') {
1264 + $customFieldMap = $this->boardService->copyCustomFieldsOfBoard($board_id, $newBoard);
1265 + }
1266 +
863 1267 //label copy
864 1268 $labelMap = [];
865 1269
866 1270 if ($isWithLabels == 'yes') {
@@ -867,15 +1271,19 @@
867 1271 $labelMap = $this->labelService->copyLabelsOfBoard($board_id, $newBoard);
868 1272 }
869 1273
870 1274 //stage copy
871 - $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id);
1275 + $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id, $isWithTemplates);
872 1276
873 1277 //copy tasks of selected stages
874 1278 if ($isWithTasks == 'yes') {
875 - $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap);
1279 + $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap, $isWithTemplates, $customFieldMap);
876 1280 }
877 1281
1282 + if ($isWithMembers == 'yes') {
1283 + $this->boardService->copyMembersOfBoard($board_id, $newBoard);
1284 + }
1285 +
878 1286 return $this->sendSuccess([
879 1287 'board' => $newBoard,
880 1288 ], 200);
881 1289 } catch (\Exception $e) {
@@ -885,11 +1293,18 @@
885 1293
886 1294 public function importFromBoard(Request $request, $board_id)
887 1295 {
888 1296 $selectedStages = $request->getSafe('selectedStages');
1297 + $position = $request->getSafe('position', 'intval');
889 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 +
890 1305 try {
891 - $this->stageService->importStagesFromBoard($board_id, $selectedStages);
1306 + $this->stageService->importStagesFromBoard($board_id, $selectedStages, $position);
892 1307
893 1308 return $this->sendSuccess([
894 1309 'message' => __('Import successfully', 'fluent-boards'),
895 1310 ], 200);
@@ -962,7 +1377,184 @@
962 1377 ? 'manager'
963 1378 : ($boardRelation && Arr::has($boardRelation->settings, 'is_viewer_only') && Arr::get($boardRelation->settings, 'is_viewer_only')
964 1379 ? 'viewer'
965 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;
966 1558 }
967 1559
968 1560 }