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 +747 -137 1.31 → 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,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) {
@@ -451,8 +742,11 @@
451 742 $boardObjects = Relation::where('object_type', 'board_user')
452 743 ->where('object_id', $board_id)
453 744 ->get()->keyBy('foreign_id');
454 745
746 + $superAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
747 + ->get()->pluck('object_id')->toArray();
748 +
455 749 $userIds = $boardObjects->pluck('foreign_id')->toArray();
456 750
457 751 $coreUsers = [];
458 752 if ($userIds) {
@@ -471,14 +765,17 @@
471 765 }
472 766
473 767 $boardRelation = $boardObjects[$user->ID] ?? null;
474 768
769 +
475 770 $formattedUsers[] = [
476 771 'ID' => $user->ID,
477 772 'display_name' => $name,
478 773 'email' => $user->user_email,
479 774 'photo' => fluent_boards_user_avatar($user->user_email, $name),
480 - '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')
481 778 ];
482 779 }
483 780
484 781 // order formatted users by display_name
@@ -495,16 +792,24 @@
495 792 return $returnData;
496 793 }
497 794
498 795 /*
499 - * 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.
500 797 */
501 - $adminUserIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
798 + $fluentBoardAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
502 799 ->whereNotIn('object_id', $userIds)
503 800 ->get()
504 801 ->pluck('object_id')
505 802 ->toArray();
506 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 +
507 812 if ($adminUserIds) {
508 813 $adminUsers = get_users([
509 814 'include' => $adminUserIds,
510 815 ]);
@@ -521,9 +826,11 @@
521 826 'ID' => $user->ID,
522 827 'display_name' => $name,
523 828 'email' => $user->user_email,
524 829 'photo' => fluent_boards_user_avatar($user->user_email, $name),
525 - 'role' => 'admin'
830 + 'role' => 'admin',
831 + 'is_super' => in_array($user->ID, $superAdminIds),
832 + 'is_wpadmin' => $user->has_cap('manage_options')
526 833 ];
527 834 }
528 835
529 836 // order formatted users by display_name
@@ -552,20 +859,28 @@
552 859 }
553 860
554 861 public function addMembersInBoard(Request $request, $board_id)
555 862 {
556 - $memberId = $request->getSafe('memberId');
557 - $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);
558 866
559 - if ($isAlreadyMember) {
867 + if ($member === null) {
560 868 return $this->sendError([
869 + 'message' => __('User not found.', 'fluent-boards'),
870 + ], 404);
871 + }
872 +
873 + if (!$member) {
874 + return $this->sendError([
561 875 'message' => __('User already a member', 'fluent-boards'),
562 - ], 304);
876 + ], 409);
563 877 }
564 - $member = $this->boardService->addMembersInBoard($board_id, $memberId);
565 878
879 +
566 880 return [
567 881 'message' => __('Member added successfully', 'fluent-boards'),
882 + 'is_admin' => PermissionManager::isAdmin($memberId),
568 883 'member' => Helper::sanitizeUserCollections($member)
569 884 ];
570 885 }
571 886
@@ -591,11 +906,11 @@
591 906 }
592 907
593 908 public function searchBoards(Request $request)
594 909 {
595 - $per_page = $request->get('per_page', 10);
596 - $search_input = $request->searchInput . trim('');
597 - $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');
598 913
599 914 $currentUserId = get_current_user_id();
600 915
601 916 if (PermissionManager::isAdmin($currentUserId)) {
@@ -637,10 +952,13 @@
637 952 * @return
638 953 */
639 954 public function changeStageView($board_id, $stage_id)
640 955 {
956 + $board_id = absint($board_id);
957 + $stage_id = absint($stage_id);
958 +
641 959 try {
642 - $stage = Stage::findOrFail($stage_id);
960 + $stage = $this->findStageOnBoard($stage_id, $board_id);
643 961 $message = __('The stage is made public!', 'fluent-boards');
644 962 $settings = $stage->settings;
645 963
646 964 if (isset($settings['is_public'])) {
@@ -645,9 +963,9 @@
645 963
646 964 if (isset($settings['is_public'])) {
647 965 if ($settings['is_public']) {
648 966 $settings['is_public'] = false;
649 - $message = __('The stage is made admin only!', 'fluent-boards');
967 + $message = __('The stage is made private!', 'fluent-boards');
650 968 } else {
651 969 $settings['is_public'] = true;
652 970 }
653 971 } else {
@@ -666,24 +984,27 @@
666 984 }
667 985
668 986
669 987 /**
670 - * Set board background image or color
988 + * Set or reset board background image/color.
671 989 * @param \FluentBoards\Framework\Http\Request\Request $request
672 990 * @return
673 991 */
674 992 public function setBoardBackground(Request $request, $board_id)
675 993 {
676 - // sanitize and validate image_url
677 - 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) {
678 1002 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
679 - "id" => 'required',
1003 + 'id' => 'required|integer',
680 1004 'image_url' => 'required|string|url',
681 1005 ]);
682 - }
683 -
684 - // sanitize and validate color
685 - if ($request->color) {
1006 + } elseif ($request->color) {
686 1007 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
687 1008 "id" => 'required',
688 1009 'color' => 'required',
689 1010 ]);
@@ -691,17 +1012,22 @@
691 1012
692 1013 try {
693 1014 if (!$board_id) {
694 1015 $errorMessage = __('Board id is required', 'fluent-boards');
695 - throw new \Exception($errorMessage, 400);
1016 + throw new \Exception(esc_html($errorMessage), 400);
696 1017 }
697 1018
1019 + if (empty($backgroundData)) {
1020 + $errorMessage = __('Background data is required', 'fluent-boards');
1021 + throw new \Exception(esc_html($errorMessage), 400);
1022 + }
1023 +
698 1024 return $this->sendSuccess([
699 1025 'message' => __('Background updated successfully', 'fluent-boards'),
700 1026 'background' => $this->boardService->setBoardBackground($backgroundData, $board_id),
701 1027 ]);
702 1028 } catch (\Exception $e) {
703 - $this->sendError([$e->getMessage(), 400]);
1029 + return $this->sendError($e->getMessage(), 400);
704 1030 }
705 1031 }
706 1032
707 1033
@@ -711,15 +1037,19 @@
711 1037 * @param mixed $stage_slug
712 1038 * @return $availablePositions as an array
713 1039 * @throws \Exception
714 1040 */
715 - public function getStageTaskAvailablePositions($board_id, $stage_id)
1041 + public function getStageTaskAvailablePositions(Request $request, $board_id, $stage_id)
716 1042 {
717 1043 try {
718 1044 if ($board_id && $stage_id) {
719 - $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);
720 1047 return $this->sendSuccess([
721 - 'availablePositions' => $availablePositions
1048 + 'availablePositions' => $availablePositions['availablePositions'],
1049 + 'moveTargets' => $availablePositions['moveTargets'],
1050 + 'currentMoveTargetKey' => $availablePositions['currentMoveTargetKey'],
1051 + 'defaultMoveTargetKey' => $availablePositions['defaultMoveTargetKey'],
722 1052 ], 200);
723 1053 } else {
724 1054 $message = '';
725 1055 if (!$board_id) {
@@ -727,12 +1057,12 @@
727 1057 }
728 1058 if (!$stage_id) {
729 1059 $message = 'Stage ';
730 1060 }
731 - throw new \Exception($message . 'is required', 400);
1061 + throw new \Exception(esc_html($message . 'is required'), 400);
732 1062 }
733 1063 } catch (\Exception $e) {
734 - $this->sendError([$e->getMessage(), 400]);
1064 + return $this->sendError($e->getMessage(), 400);
735 1065 }
736 1066 }
737 1067
738 1068 public function getAssociateCrmContacts($board_id)
@@ -741,24 +1071,47 @@
741 1071 $contactAssociatedTasks = Task::with('board')->where('board_id', $board_id)
742 1072 ->whereNotNull('crm_contact_id')
743 1073 ->get();
744 1074
745 - $formattedContacts = Collection::make($contactAssociatedTasks)
746 - ->groupBy('crm_contact_id')
747 - ->map(function ($tasks, $contactId) {
748 - $subscriber = Subscriber::find($contactId);
749 - 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) {
750 1104 return null; // Skip if subscriber not found
751 1105 }
752 1106
753 - return [
754 - 'name' => $subscriber->first_name . ' ' . $subscriber->last_name,
755 - 'photo' => $subscriber->photo,
756 - 'email' => $subscriber->email,
757 - 'crm_contact_id' => $contactId,
758 - 'id' => $contactId,
759 - 'tasks' => $tasks,
760 - ];
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;
761 1114 })
762 1115 ->filter()->toArray();
763 1116
764 1117
@@ -779,11 +1132,13 @@
779 1132 'message' => __('Associated Crm Member has been updated', 'fluent-boards'),
780 1133 ], 200);
781 1134 }
782 1135
783 - public function hasDataChanged($board_id)
1136 + public function hasDataChanged(Request $request, $board_id)
784 1137 {
785 - 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);
786 1141 }
787 1142
788 1143 public function createStage(Request $request, $board_id)
789 1144 {
@@ -788,8 +1143,9 @@
788 1143 public function createStage(Request $request, $board_id)
789 1144 {
790 1145 $stageData = $this->stageSanitizeAndValidate($request->all(), [
791 1146 'title' => 'required|string',
1147 + 'position' => 'nullable|numeric'
792 1148 ]);
793 1149
794 1150 $board = Board::find($board_id);
795 1151 $stage = $this->stageService->createStage($stageData, $board_id);
@@ -805,15 +1161,27 @@
805 1161 }
806 1162
807 1163 public function moveAllTasks(Request $request, $board_id)
808 1164 {
809 - $oldStageId = $request->getSafe('oldStageId');
810 - $newStageId = $request->getSafe('newStageId');
1165 + $oldStageId = $request->getSafe('oldStageId', 'intval');
1166 + $newStageId = $request->getSafe('newStageId', 'intval');
811 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 +
812 1180 $updates = $this->stageService->moveAllTasks($oldStageId, $newStageId, $board_id);
813 1181
814 1182 return [
815 - 'message' => __('Tasks has been Moved', 'fluent-boards'),
1183 + 'message' => __('Tasks have been moved', 'fluent-boards'),
816 1184 'updatedTasks' => $updates,
817 1185 ];
818 1186
819 1187 }
@@ -819,39 +1187,84 @@
819 1187 }
820 1188
821 1189 public function archiveAllTasksInStage($board_id, $stage_id)
822 1190 {
823 - $updates = $this->stageService->archiveAllTasksInStage($stage_id);
824 - return [
825 - 'message' => __('Tasks has been archived', 'fluent-boards'),
826 - 'updatedTasks' => $updates,
827 - ];
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 + }
828 1205 }
829 1206
830 1207 public function getAssociatedBoards(Request $request, $associated_id)
831 1208 {
832 - $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 +
833 1221 return [
834 1222 'boards' => $associatedBoards,
835 1223 ];
836 1224 }
837 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 + */
838 1240 public function duplicateBoard(Request $request, $board_id)
839 1241 {
840 1242 $boardData = $this->taskSanitizeAndValidate($request->get('board'), [
841 1243 'title' => 'required|string'
842 1244 ]);
1245 +
1246 + $boardData['source_board_id'] = $board_id;
1247 +
843 1248 $isWithLabels = $request->getSafe('isWithLabels');
844 1249 $isWithTasks = $request->getSafe('isWithTasks');
1250 + $isWithMembers = $request->getSafe('isWithMembers');
1251 + $isWithCustomFields = $request->getSafe('isWithCustomFields', 'sanitize_text_field', 'yes');
1252 + $isWithTemplates = $request->getSafe('isWithTemplates');
845 1253
846 1254 try {
847 1255 if(!PermissionManager::isAdmin()) {
848 1256 $errorMessage = __('You do not have permission to duplicate board', 'fluent-boards');
849 - throw new \Exception($errorMessage, 400);
1257 + throw new \Exception(esc_html($errorMessage), 400);
850 1258 }
851 1259 //create board
852 1260 $newBoard = $this->boardService->copyBoard($boardData);
1261 + $customFieldMap = [];
853 1262
1263 + if ($isWithCustomFields == 'yes') {
1264 + $customFieldMap = $this->boardService->copyCustomFieldsOfBoard($board_id, $newBoard);
1265 + }
1266 +
854 1267 //label copy
855 1268 $labelMap = [];
856 1269
857 1270 if ($isWithLabels == 'yes') {
@@ -858,15 +1271,19 @@
858 1271 $labelMap = $this->labelService->copyLabelsOfBoard($board_id, $newBoard);
859 1272 }
860 1273
861 1274 //stage copy
862 - $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id);
1275 + $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id, $isWithTemplates);
863 1276
864 1277 //copy tasks of selected stages
865 1278 if ($isWithTasks == 'yes') {
866 - $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap);
1279 + $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap, $isWithTemplates, $customFieldMap);
867 1280 }
868 1281
1282 + if ($isWithMembers == 'yes') {
1283 + $this->boardService->copyMembersOfBoard($board_id, $newBoard);
1284 + }
1285 +
869 1286 return $this->sendSuccess([
870 1287 'board' => $newBoard,
871 1288 ], 200);
872 1289 } catch (\Exception $e) {
@@ -876,11 +1293,18 @@
876 1293
877 1294 public function importFromBoard(Request $request, $board_id)
878 1295 {
879 1296 $selectedStages = $request->getSafe('selectedStages');
1297 + $position = $request->getSafe('position', 'intval');
880 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 +
881 1305 try {
882 - $this->stageService->importStagesFromBoard($board_id, $selectedStages);
1306 + $this->stageService->importStagesFromBoard($board_id, $selectedStages, $position);
883 1307
884 1308 return $this->sendSuccess([
885 1309 'message' => __('Import successfully', 'fluent-boards'),
886 1310 ], 200);
@@ -944,7 +1368,193 @@
944 1368 ];
945 1369 } catch (\Exception $e) {
946 1370 return $this->sendError($e->getMessage(), 400);
947 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;
948 1558 }
949 1559
950 1560 }