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 +798 -136 1.23 → 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,22 +55,66 @@
45 55 }
46 56
47 57 public function getBoards(Request $request)
48 58 {
49 - $per_page = $request->getSafe('per_page', 'intval', 20);
59 + $per_page = $request->getSafe('per_page', 'intval', 100);
60 + $per_page = max(1, min(100, $per_page));
50 61 $userId = get_current_user_id();
51 62 $type = $request->getSafe('type', 'sanitize_text_field', 'to-do');
52 63
53 64 $order = $request->getSafe('order', 'sanitize_text_field', 'created_at');
54 65 $orderBy = $request->getSafe('orderBy', 'sanitize_text_field', 'DESC');
66 + $order = is_string($order) ? sanitize_sql_orderby($order) : false;
67 + if (!in_array($order, ['created_at', 'title', 'updated_at', 'id'], true)) {
68 + $order = 'created_at';
69 + }
70 + $orderBy = is_string($orderBy) ? strtoupper($orderBy) : 'DESC';
71 + if (!in_array($orderBy, ['ASC', 'DESC'], true)) {
72 + $orderBy = 'DESC';
73 + }
74 +
55 75 $searchInput = $request->getSafe('searchInput', 'sanitize_text_field');
56 76
57 - if(!defined('FLUENT_ROADMAP')) {
58 - $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
77 + $option = $request->getSafe('option', 'sanitize_text_field');
78 + $folderId = $request->getSafe('fid', 'intval'); // Get folder ID from request
79 +
80 + // Initialize the query based on archive status
81 + if (!defined('FLUENT_ROADMAP')) {
82 + if ($option == 'archived') {
83 + $relatedBoardsQuery = Board::whereNotNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
84 + } else {
85 + $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
86 + }
59 87 } else {
60 - $relatedBoardsQuery = Board::whereNull('archived_at')->byAccessUser($userId);
88 + if ($option == 'archived') {
89 + $relatedBoardsQuery = Board::whereNotNull('archived_at')->byAccessUser($userId);
90 + } else {
91 + $relatedBoardsQuery = Board::whereNull('archived_at')->byAccessUser($userId);
92 + }
61 93 }
62 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
63 117 if (!empty($searchInput)) {
64 118 $relatedBoardsQuery = $relatedBoardsQuery->where('title', 'like', '%' . $searchInput . '%');
65 119 }
66 120
@@ -65,21 +119,63 @@
65 119 }
66 120
67 121 $relatedBoards = $relatedBoardsQuery->orderBy($order, $orderBy)
68 122 ->withCount('completedTasks')
69 - ->with('stages', 'users')
123 + ->with('stages', 'users', 'boardMeta', 'viewerMembership')
70 124 ->paginate($per_page);
71 125
72 126 foreach ($relatedBoards as $relatedBoard) {
127 + $relatedBoard->description = DescriptionMarkdownConverter::normalize($relatedBoard->description);
73 128 $relatedBoard->users = Helper::sanitizeUserCollections($relatedBoard->users);
129 + $relatedBoard->is_pinned = in_array((int) $relatedBoard->id, $pinnedIds, true);
74 130 }
75 131
76 - return $this->sendSuccess([
77 - 'boards' => $relatedBoards
78 - ], 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);
79 146 }
80 147
81 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 + /**
82 178 * Get the list of boards and their associated stages for the current user.
83 179 *
84 180 * @param \FluentBoards\Framework\Http\Request\Request $request
85 181 * @return \WP_REST_Response
@@ -85,30 +181,56 @@
85 181 * @return \WP_REST_Response
86 182 */
87 183 public function getBoardsList(Request $request)
88 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 + {
89 207 try {
90 208 $userId = get_current_user_id();
91 209
92 - // Query to fetch boards that are not archived and accessible by the user
93 - // Check if the FLUENT_ROADMAP constant is defined
94 - if (!defined('FLUENT_ROADMAP')) {
95 - $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
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);
96 216 } else {
97 - $relatedBoardsQuery = Board::whereNull('archived_at')->byAccessUser($userId);
217 + $relatedBoardsQuery = Board::whereNull('archived_at')->excludeTemplates()->byAccessUser($userId);
98 218 }
99 219
100 - $relatedBoards = $relatedBoardsQuery->with('stages')->get();
220 + if (!empty($searchInput)) {
221 + $relatedBoardsQuery = $relatedBoardsQuery->where('title', 'like', '%' . $searchInput . '%');
222 + }
101 223
102 - // Fetch the stages associated with the boards
103 - $stages = Stage::whereIn('board_id', $relatedBoards->pluck('id'))->where('archived_at', null)->get();
224 + $relatedBoards = $relatedBoardsQuery->orderBy('created_at', 'DESC')->get();
104 225
105 226 return $this->sendSuccess([
106 - 'boards' => $relatedBoards,
107 - 'all_stages' => $stages,
108 - ], 200);
227 + 'boards' => $relatedBoards
228 + ]);
109 229 } catch (\Exception $e) {
110 - return $this->sendError($e->getMessage(), 404);
230 + return $this->sendError([
231 + 'message' => $e->getMessage()
232 + ]);
111 233 }
112 234 }
113 235
114 236 public function getRecentBoards()
@@ -115,9 +237,12 @@
115 237 {
116 238 $boards = $this->boardService->getRecentBoards();
117 239
118 240 if (!$boards || $boards->isEmpty()) {
119 - $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())
120 245 ->limit(4)
121 246 ->withCount('completedTasks')
122 247 ->with(['stages', 'users'])
123 248 ->get();
@@ -124,8 +249,9 @@
124 249 }
125 250
126 251 foreach ($boards as $board) {
127 252 $board->users = Helper::sanitizeUserCollections($board->users);
253 + $board->is_pinned = $this->boardService->isPinned($board->id);
128 254 }
129 255
130 256 return [
131 257 'boards' => $boards,
@@ -140,9 +266,9 @@
140 266 $boards = $this->boardService->getBoardsByType($type);
141 267
142 268 return $this->sendSuccess([
143 269 'boards' => $boards,
144 - ], 200);
270 + ]);
145 271 }
146 272
147 273 public function createFirstBoard(Request $request)
148 274 {
@@ -153,11 +279,14 @@
153 279 'currency' => 'nullable|string',
154 280 'crm_contact_id' => 'nullable|numeric',
155 281 ]);
156 282
157 - $installFluentCRM = $request->get('withFluentCRM') == 'yes' ? true : false;
283 + $installFluentCRM = $request->getSafe('withFluentCRM', 'sanitize_text_field') == 'yes' ? true : false;
158 284
159 285 $postStages = $request->get('stages');
286 + if (!is_array($postStages)) {
287 + $postStages = [];
288 + }
160 289 $stageData = array();
161 290 foreach ($postStages as $stage) {
162 291 $temp = $this->stageSanitizeAndValidate($stage, [
163 292 'title' => 'required|string',
@@ -167,9 +296,9 @@
167 296
168 297 $taskData = null;
169 298 if ($request->get('task')) {
170 299 $taskData = $this->taskSanitizeAndValidate($request->get('task'), [
171 - 'title' => 'required|string'
300 + 'title' => 'required|string',
172 301 ]);
173 302 }
174 303
175 304 $board = $this->boardService->createBoard($boardData);
@@ -183,9 +312,9 @@
183 312 $this->taskService->createTask($taskData, $board->id);
184 313 }
185 314
186 315 do_action('fluent_boards/board_created', $board);
187 -
316 +
188 317 if ($installFluentCRM && !defined('FLUENTCRM')) {
189 318 InstallService::install('fluent-crm');
190 319 }
191 320
@@ -194,8 +323,21 @@
194 323 'board' => $board,
195 324 ];
196 325 }
197 326
327 + public function skipOnboarding(Request $request)
328 + {
329 + $onboarding = Meta::where('key', Constant::FBS_ONBOARDING)->first();
330 + if($onboarding && $onboarding->value == 'no'){
331 + $onboarding->value = 'yes' ;
332 + $onboarding->save();
333 + }
334 +
335 + return [
336 + 'message' => __('Onboarding skipped successfully', 'fluent-boards'),
337 + ];
338 + }
339 +
198 340 public function create(Request $request)
199 341 {
200 342 $boardData = $this->boardSanitizeAndValidate($request->get('board'), [
201 343 'title' => 'required|string',
@@ -202,17 +344,46 @@
202 344 'description' => 'nullable',
203 345 'type' => 'required|string',
204 346 'currency' => 'nullable|string',
205 347 'crm_contact_id' => 'nullable|numeric',
348 + 'folder_id' => 'nullable',
206 349 ]);
207 350
208 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 +
209 365 $board = $this->boardService->createBoard($boardData);
210 - $this->labelService->createDefaultLabel($board->id);
366 + $this->createBoardLabelsFromRequest($request, $board->id);
367 + $this->addBoardMembersFromRequest($request, $board->id);
211 368 $type = ucfirst($boardData['type']);
369 + $stages = $request->get('stages');
370 + $sanitizedStages = [];
212 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 +
213 382 if (isset($boardData['type']) && $boardData['type'] == 'roadmap') {
214 - $this->stageService->createRoadmapStages($board, $request->get('stages'));
383 + $this->stageService->createRoadmapStages($board, $sanitizedStages);
384 + } elseif (!empty($sanitizedStages)) {
385 + $this->stageService->createStages($board, $sanitizedStages);
215 386 } else {
216 387 $this->stageService->createDefaultStages($board);
217 388 }
218 389
@@ -222,37 +393,151 @@
222 393 }
223 394
224 395 do_action('fluent_boards/board_created', $board);
225 396
397 +
398 + if ($folderId) {
399 + $folderService->addBoardToFolder($folderId, [$board->id]);
400 + }
401 +
226 402 $message = __('Board has been created successfully', 'fluent-boards');
227 403
228 - return $this->sendSuccess([
404 + return $this->send([
229 405 'message' => $message,
230 406 'board' => $board,
231 407 ], 201);
232 408 } catch (\Exception $e) {
233 - return $this->sendError($e->getMessage(), 400);
409 + return $this->sendError([
410 + 'message' => $e->getMessage()
411 + ]);
234 412 }
235 413 }
236 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 + */
237 528 public function getArchivedStage(Request $request, $board_id)
238 529 {
239 530 try {
240 - $pagination = $request->noPagination ? true : false;
241 - $per_page = isset($data['per_page']) ? $data['per_page'] : 30;
242 - $page = isset($data['page']) ? $data['page'] : 1;
243 - if ($pagination) {
244 - $stages = Stage::where('board_id', $board_id)
245 - ->whereNotNull('archived_at')
246 - ->orderBy('created_at', 'DESC')
247 - ->get();
248 - } else {
249 - $stages = Stage::where('board_id', $board_id)
250 - ->whereNotNull('archived_at')
251 - ->orderBy('created_at', 'DESC')
252 - ->paginate($per_page, ['*'], 'page', $page);
253 - }
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 + ];
254 537
538 + $stages = $this->stageService->getArchivedStages($sanitizedParams, $board_id);
539 +
255 540 return $this->sendSuccess([
256 541 'stages' => $stages,
257 542 ], 200);
258 543 } catch (\Exception $e) {
@@ -259,17 +544,35 @@
259 544 return $this->sendError($e->getMessage(), 404);
260 545 }
261 546 }
262 547
263 - 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)
264 550 {
551 + $syncStartedAt = current_time('mysql');
265 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);
266 555 $board->background = maybe_unserialize($board->background);
267 556 $board->createdOn = $board->created_at->format('Y-m-d');
268 557
269 - $board->load(['users', 'stages', 'labels', 'owner']);
558 + $board->load(['users', 'labels', 'owner']);
270 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 +
271 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 +
272 575 $board->load(['customFields']);
273 576 }
274 577
275 578 $this->boardService->updateRecentBoards($board_id);
@@ -275,21 +578,35 @@
275 578 $this->boardService->updateRecentBoards($board_id);
276 579
277 580 $board->labelColor = Constant::TRELLO_COLOR_MAP;
278 581 $board->labelColorText = Constant::TEXT_COLOR_MAP;
582 + $board->labelColorPresets = Constant::LABEL_COLOR_PRESETS;
279 583
280 584 $board->users = Helper::sanitizeUserCollections($board->users);
281 585 $board->owner = Helper::sanitizeUserCollections($board->owner);
282 586
587 + $board->is_pinned = $this->boardService->isPinned($board->id);
588 +
283 589 $board = apply_filters('fluent_boards/board_find', $board);
284 590
285 591 return [
286 - 'board' => $board
592 + 'board' => $board,
593 + 'synced_at' => $syncStartedAt
287 594 ];
288 595 }
289 596
290 597 public function update(Request $request, $board_id)
291 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 +
292 609 $boardData = $this->boardSanitizeAndValidate($request->only(['title', 'description']), [
293 610 'title' => 'required|string',
294 611 'description' => 'nullable|string',
295 612 ]);
@@ -294,8 +611,9 @@
294 611 'description' => 'nullable|string',
295 612 ]);
296 613
297 614 $board = Board::findOrFail($board_id);
615 + $boardData['description'] = DescriptionMarkdownConverter::normalize($boardData['description']);
298 616
299 617 $oldBoard = clone $board;
300 618 $board->fill($boardData);
301 619 $board->save();
@@ -302,21 +620,23 @@
302 620
303 621 do_action('fluent_boards/board_updated', $board, $oldBoard);
304 622
305 623 return [
306 - 'stages' => $board->stages()->get(),
307 624 'message' => __('Board has been updated', 'fluent-boards'),
308 625 'board' => $board,
626 + 'stages' => $board->stages()->get(),
309 627 ];
310 628 }
311 629
312 630 public function archiveStage($board_id, $stage_id)
313 631 {
632 + $board_id = absint($board_id);
633 + $stage_id = absint($stage_id);
634 +
314 635 try {
315 - $stage = Stage::findOrFail($stage_id);
316 - $board = Board::findOrFail($stage->board_id);
636 + $stage = $this->findStageOnBoard($stage_id, $board_id);
317 637
318 - $updatedStage = $this->boardService->archiveStage($board->id, $stage);
638 + $updatedStage = $this->boardService->archiveStage($board_id, $stage);
319 639
320 640 return $this->sendSuccess([
321 641 'updatedStage' => $updatedStage,
322 642 'message' => __('Stage has been archived', 'fluent-boards'),
@@ -327,18 +647,20 @@
327 647 }
328 648
329 649 public function restoreStage($board_id, $stage_id)
330 650 {
651 + $board_id = absint($board_id);
652 + $stage_id = absint($stage_id);
653 +
331 654 try {
332 - $stage = Stage::findOrFail($stage_id);
333 - $board = Board::findOrFail($board_id);
655 + $stage = $this->findStageOnBoard($stage_id, $board_id);
334 656
335 - $updatedStage = $this->boardService->restoreStage($board->id, $stage);
657 + $updatedStage = $this->boardService->restoreStage($board_id, $stage);
336 658
337 659 return $this->sendSuccess([
338 - 'success' => true,
660 + 'success' => true,
339 661 'updatedStage' => $updatedStage,
340 - 'message' => __('Stage has been restored', 'fluent-boards')
662 + 'message' => __('Stage has been restored', 'fluent-boards')
341 663 ], 200);
342 664 } catch (\Exception $e) {
343 665 return $this->sendError($e->getMessage(), 400);
344 666 }
@@ -344,32 +666,22 @@
344 666 }
345 667 }
346 668
347 669
348 - public function changePositionOfStage(Request $request, $board_id)
670 + public function repositionStages(Request $request, $board_id)
349 671 {
350 - $changeData = $this->boardSanitizeAndValidate($request->only(['fromPosition', 'toPosition']), [
351 - 'fromPosition' => 'required',
352 - 'toPosition' => 'required',
353 - ]);
354 -
672 + $incomingList = $request->get('list');
673 + if (!is_array($incomingList)) {
674 + $incomingList = [];
675 + }
676 + $incomingList = array_map('intval', $incomingList);
355 677 try {
356 - $this->boardService->changePositionOfStage($board_id, $changeData);
678 + foreach ($incomingList as $stageId) {
679 + $this->findStageOnBoard($stageId, $board_id);
680 + }
357 681
682 + $this->boardService->repositionStages($board_id, $incomingList);
358 683 return $this->sendSuccess([
359 - 'message' => __('Board stage has been updated', 'fluent-boards')
360 - ], 200);
361 - } catch (\Exception $e) {
362 - return $this->sendError($e->getMessage(), 400);
363 - }
364 - }
365 -
366 - public function rePositionStages(Request $request, $board_id)
367 - {
368 - $incomingList = $request->get('list');
369 - try {
370 - $this->boardService->rePositionStages($board_id, $incomingList);
371 - return $this->sendSuccess([
372 684 'message' => __('Stages Reordered', 'fluent-boards'),
373 685 'updatedStages' => $this->stageService->getLastOneMinuteUpdatedStages($board_id)
374 686 ], 200);
375 687 } catch (\Exception $e) {
@@ -387,9 +699,9 @@
387 699 public function delete($board_id)
388 700 {
389 701 try {
390 702 if (!PermissionManager::isAdmin()) {
391 - 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);
392 704 }
393 705 $this->boardService->deleteBoard($board_id);
394 706
395 707 return $this->sendSuccess([
@@ -407,9 +719,12 @@
407 719
408 720 public function getActivities(Request $request, $board_id)
409 721 {
410 722 try {
411 - $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 + ]);
412 727 return $this->sendSuccess([
413 728 'activities' => $activities,
414 729 ], 200);
415 730 } catch (\Exception $e) {
@@ -427,8 +742,11 @@
427 742 $boardObjects = Relation::where('object_type', 'board_user')
428 743 ->where('object_id', $board_id)
429 744 ->get()->keyBy('foreign_id');
430 745
746 + $superAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
747 + ->get()->pluck('object_id')->toArray();
748 +
431 749 $userIds = $boardObjects->pluck('foreign_id')->toArray();
432 750
433 751 $coreUsers = [];
434 752 if ($userIds) {
@@ -447,14 +765,17 @@
447 765 }
448 766
449 767 $boardRelation = $boardObjects[$user->ID] ?? null;
450 768
769 +
451 770 $formattedUsers[] = [
452 771 'ID' => $user->ID,
453 772 'display_name' => $name,
454 773 'email' => $user->user_email,
455 774 'photo' => fluent_boards_user_avatar($user->user_email, $name),
456 - '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')
457 778 ];
458 779 }
459 780
460 781 // order formatted users by display_name
@@ -471,16 +792,24 @@
471 792 return $returnData;
472 793 }
473 794
474 795 /*
475 - * 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.
476 797 */
477 - $adminUserIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
798 + $fluentBoardAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
478 799 ->whereNotIn('object_id', $userIds)
479 800 ->get()
480 801 ->pluck('object_id')
481 802 ->toArray();
482 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 +
483 812 if ($adminUserIds) {
484 813 $adminUsers = get_users([
485 814 'include' => $adminUserIds,
486 815 ]);
@@ -497,9 +826,11 @@
497 826 'ID' => $user->ID,
498 827 'display_name' => $name,
499 828 'email' => $user->user_email,
500 829 'photo' => fluent_boards_user_avatar($user->user_email, $name),
501 - 'role' => 'admin'
830 + 'role' => 'admin',
831 + 'is_super' => in_array($user->ID, $superAdminIds),
832 + 'is_wpadmin' => $user->has_cap('manage_options')
502 833 ];
503 834 }
504 835
505 836 // order formatted users by display_name
@@ -528,20 +859,28 @@
528 859 }
529 860
530 861 public function addMembersInBoard(Request $request, $board_id)
531 862 {
532 - $memberId = $request->getSafe('memberId');
533 - $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);
534 866
535 - if ($isAlreadyMember) {
867 + if ($member === null) {
536 868 return $this->sendError([
869 + 'message' => __('User not found.', 'fluent-boards'),
870 + ], 404);
871 + }
872 +
873 + if (!$member) {
874 + return $this->sendError([
537 875 'message' => __('User already a member', 'fluent-boards'),
538 - ], 304);
876 + ], 409);
539 877 }
540 - $member = $this->boardService->addMembersInBoard($board_id, $memberId);
541 878
879 +
542 880 return [
543 881 'message' => __('Member added successfully', 'fluent-boards'),
882 + 'is_admin' => PermissionManager::isAdmin($memberId),
544 883 'member' => Helper::sanitizeUserCollections($member)
545 884 ];
546 885 }
547 886
@@ -567,11 +906,11 @@
567 906 }
568 907
569 908 public function searchBoards(Request $request)
570 909 {
571 - $per_page = $request->get('per_page', 10);
572 - $search_input = $request->searchInput . trim('');
573 - $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');
574 913
575 914 $currentUserId = get_current_user_id();
576 915
577 916 if (PermissionManager::isAdmin($currentUserId)) {
@@ -613,10 +952,13 @@
613 952 * @return
614 953 */
615 954 public function changeStageView($board_id, $stage_id)
616 955 {
956 + $board_id = absint($board_id);
957 + $stage_id = absint($stage_id);
958 +
617 959 try {
618 - $stage = Stage::findOrFail($stage_id);
960 + $stage = $this->findStageOnBoard($stage_id, $board_id);
619 961 $message = __('The stage is made public!', 'fluent-boards');
620 962 $settings = $stage->settings;
621 963
622 964 if (isset($settings['is_public'])) {
@@ -621,9 +963,9 @@
621 963
622 964 if (isset($settings['is_public'])) {
623 965 if ($settings['is_public']) {
624 966 $settings['is_public'] = false;
625 - $message = __('The stage is made admin only!', 'fluent-boards');
967 + $message = __('The stage is made private!', 'fluent-boards');
626 968 } else {
627 969 $settings['is_public'] = true;
628 970 }
629 971 } else {
@@ -642,24 +984,27 @@
642 984 }
643 985
644 986
645 987 /**
646 - * Set board background image or color
988 + * Set or reset board background image/color.
647 989 * @param \FluentBoards\Framework\Http\Request\Request $request
648 990 * @return
649 991 */
650 992 public function setBoardBackground(Request $request, $board_id)
651 993 {
652 - // sanitize and validate image_url
653 - 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) {
654 1002 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
655 - "id" => 'required',
1003 + 'id' => 'required|integer',
656 1004 'image_url' => 'required|string|url',
657 1005 ]);
658 - }
659 -
660 - // sanitize and validate color
661 - if ($request->color) {
1006 + } elseif ($request->color) {
662 1007 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
663 1008 "id" => 'required',
664 1009 'color' => 'required',
665 1010 ]);
@@ -667,17 +1012,22 @@
667 1012
668 1013 try {
669 1014 if (!$board_id) {
670 1015 $errorMessage = __('Board id is required', 'fluent-boards');
671 - throw new \Exception($errorMessage, 400);
1016 + throw new \Exception(esc_html($errorMessage), 400);
672 1017 }
673 1018
1019 + if (empty($backgroundData)) {
1020 + $errorMessage = __('Background data is required', 'fluent-boards');
1021 + throw new \Exception(esc_html($errorMessage), 400);
1022 + }
1023 +
674 1024 return $this->sendSuccess([
675 1025 'message' => __('Background updated successfully', 'fluent-boards'),
676 1026 'background' => $this->boardService->setBoardBackground($backgroundData, $board_id),
677 1027 ]);
678 1028 } catch (\Exception $e) {
679 - $this->sendError([$e->getMessage(), 400]);
1029 + return $this->sendError($e->getMessage(), 400);
680 1030 }
681 1031 }
682 1032
683 1033
@@ -687,15 +1037,19 @@
687 1037 * @param mixed $stage_slug
688 1038 * @return $availablePositions as an array
689 1039 * @throws \Exception
690 1040 */
691 - public function getStageTaskAvailablePositions($board_id, $stage_id)
1041 + public function getStageTaskAvailablePositions(Request $request, $board_id, $stage_id)
692 1042 {
693 1043 try {
694 1044 if ($board_id && $stage_id) {
695 - $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);
696 1047 return $this->sendSuccess([
697 - 'availablePositions' => $availablePositions
1048 + 'availablePositions' => $availablePositions['availablePositions'],
1049 + 'moveTargets' => $availablePositions['moveTargets'],
1050 + 'currentMoveTargetKey' => $availablePositions['currentMoveTargetKey'],
1051 + 'defaultMoveTargetKey' => $availablePositions['defaultMoveTargetKey'],
698 1052 ], 200);
699 1053 } else {
700 1054 $message = '';
701 1055 if (!$board_id) {
@@ -703,12 +1057,12 @@
703 1057 }
704 1058 if (!$stage_id) {
705 1059 $message = 'Stage ';
706 1060 }
707 - throw new \Exception($message . 'is required', 400);
1061 + throw new \Exception(esc_html($message . 'is required'), 400);
708 1062 }
709 1063 } catch (\Exception $e) {
710 - $this->sendError([$e->getMessage(), 400]);
1064 + return $this->sendError($e->getMessage(), 400);
711 1065 }
712 1066 }
713 1067
714 1068 public function getAssociateCrmContacts($board_id)
@@ -717,24 +1071,47 @@
717 1071 $contactAssociatedTasks = Task::with('board')->where('board_id', $board_id)
718 1072 ->whereNotNull('crm_contact_id')
719 1073 ->get();
720 1074
721 - $formattedContacts = Collection::make($contactAssociatedTasks)
722 - ->groupBy('crm_contact_id')
723 - ->map(function ($tasks, $contactId) {
724 - $subscriber = Subscriber::find($contactId);
725 - 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) {
726 1104 return null; // Skip if subscriber not found
727 1105 }
728 1106
729 - return [
730 - 'name' => $subscriber->first_name . ' ' . $subscriber->last_name,
731 - 'photo' => $subscriber->photo,
732 - 'email' => $subscriber->email,
733 - 'crm_contact_id' => $contactId,
734 - 'id' => $contactId,
735 - 'tasks' => $tasks,
736 - ];
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;
737 1114 })
738 1115 ->filter()->toArray();
739 1116
740 1117
@@ -755,11 +1132,13 @@
755 1132 'message' => __('Associated Crm Member has been updated', 'fluent-boards'),
756 1133 ], 200);
757 1134 }
758 1135
759 - public function hasDataChanged($board_id)
1136 + public function hasDataChanged(Request $request, $board_id)
760 1137 {
761 - 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);
762 1141 }
763 1142
764 1143 public function createStage(Request $request, $board_id)
765 1144 {
@@ -764,8 +1143,9 @@
764 1143 public function createStage(Request $request, $board_id)
765 1144 {
766 1145 $stageData = $this->stageSanitizeAndValidate($request->all(), [
767 1146 'title' => 'required|string',
1147 + 'position' => 'nullable|numeric'
768 1148 ]);
769 1149
770 1150 $board = Board::find($board_id);
771 1151 $stage = $this->stageService->createStage($stageData, $board_id);
@@ -781,15 +1161,27 @@
781 1161 }
782 1162
783 1163 public function moveAllTasks(Request $request, $board_id)
784 1164 {
785 - $oldStageId = $request->getSafe('oldStageId');
786 - $newStageId = $request->getSafe('newStageId');
1165 + $oldStageId = $request->getSafe('oldStageId', 'intval');
1166 + $newStageId = $request->getSafe('newStageId', 'intval');
787 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 +
788 1180 $updates = $this->stageService->moveAllTasks($oldStageId, $newStageId, $board_id);
789 1181
790 1182 return [
791 - 'message' => __('Tasks has been Moved', 'fluent-boards'),
1183 + 'message' => __('Tasks have been moved', 'fluent-boards'),
792 1184 'updatedTasks' => $updates,
793 1185 ];
794 1186
795 1187 }
@@ -795,39 +1187,84 @@
795 1187 }
796 1188
797 1189 public function archiveAllTasksInStage($board_id, $stage_id)
798 1190 {
799 - $updates = $this->stageService->archiveAllTasksInStage($stage_id);
800 - return [
801 - 'message' => __('Tasks has been archived', 'fluent-boards'),
802 - 'updatedTasks' => $updates,
803 - ];
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 + }
804 1205 }
805 1206
806 1207 public function getAssociatedBoards(Request $request, $associated_id)
807 1208 {
808 - $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 +
809 1221 return [
810 1222 'boards' => $associatedBoards,
811 1223 ];
812 1224 }
813 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 + */
814 1240 public function duplicateBoard(Request $request, $board_id)
815 1241 {
816 1242 $boardData = $this->taskSanitizeAndValidate($request->get('board'), [
817 1243 'title' => 'required|string'
818 1244 ]);
1245 +
1246 + $boardData['source_board_id'] = $board_id;
1247 +
819 1248 $isWithLabels = $request->getSafe('isWithLabels');
820 1249 $isWithTasks = $request->getSafe('isWithTasks');
1250 + $isWithMembers = $request->getSafe('isWithMembers');
1251 + $isWithCustomFields = $request->getSafe('isWithCustomFields', 'sanitize_text_field', 'yes');
1252 + $isWithTemplates = $request->getSafe('isWithTemplates');
821 1253
822 1254 try {
823 1255 if(!PermissionManager::isAdmin()) {
824 1256 $errorMessage = __('You do not have permission to duplicate board', 'fluent-boards');
825 - throw new \Exception($errorMessage, 400);
1257 + throw new \Exception(esc_html($errorMessage), 400);
826 1258 }
827 1259 //create board
828 1260 $newBoard = $this->boardService->copyBoard($boardData);
1261 + $customFieldMap = [];
829 1262
1263 + if ($isWithCustomFields == 'yes') {
1264 + $customFieldMap = $this->boardService->copyCustomFieldsOfBoard($board_id, $newBoard);
1265 + }
1266 +
830 1267 //label copy
831 1268 $labelMap = [];
832 1269
833 1270 if ($isWithLabels == 'yes') {
@@ -834,15 +1271,19 @@
834 1271 $labelMap = $this->labelService->copyLabelsOfBoard($board_id, $newBoard);
835 1272 }
836 1273
837 1274 //stage copy
838 - $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id);
1275 + $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id, $isWithTemplates);
839 1276
840 1277 //copy tasks of selected stages
841 1278 if ($isWithTasks == 'yes') {
842 - $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap);
1279 + $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap, $isWithTemplates, $customFieldMap);
843 1280 }
844 1281
1282 + if ($isWithMembers == 'yes') {
1283 + $this->boardService->copyMembersOfBoard($board_id, $newBoard);
1284 + }
1285 +
845 1286 return $this->sendSuccess([
846 1287 'board' => $newBoard,
847 1288 ], 200);
848 1289 } catch (\Exception $e) {
@@ -852,11 +1293,18 @@
852 1293
853 1294 public function importFromBoard(Request $request, $board_id)
854 1295 {
855 1296 $selectedStages = $request->getSafe('selectedStages');
1297 + $position = $request->getSafe('position', 'intval');
856 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 +
857 1305 try {
858 - $this->stageService->importStagesFromBoard($board_id, $selectedStages);
1306 + $this->stageService->importStagesFromBoard($board_id, $selectedStages, $position);
859 1307
860 1308 return $this->sendSuccess([
861 1309 'message' => __('Import successfully', 'fluent-boards'),
862 1310 ], 200);
@@ -892,7 +1340,221 @@
892 1340 return [
893 1341 'message' => __('Board has been updated', 'fluent-boards'),
894 1342 'board' => apply_filters('fluent_boards/board_find', $board)
895 1343 ];
1344 + }
1345 +
1346 + public function archiveBoard($board_id)
1347 + {
1348 + try {
1349 + $board = $this->boardService->archiveBoard($board_id);
1350 +
1351 + return [
1352 + 'board' => $board,
1353 + 'message' => __('Board has been archived successfully!', 'fluent-boards')
1354 + ];
1355 + } catch (\Exception $e) {
1356 + return $this->sendError($e->getMessage(), 400);
1357 + }
1358 + }
1359 +
1360 + public function restoreBoard($board_id)
1361 + {
1362 + try {
1363 + $board = $this->boardService->restoreBoard($board_id);
1364 +
1365 + return [
1366 + 'board' => $board,
1367 + 'message' => __('Board has been restored successfully!', 'fluent-boards')
1368 + ];
1369 + } catch (\Exception $e) {
1370 + return $this->sendError($e->getMessage(), 400);
1371 + }
1372 + }
1373 +
1374 + private function boardUserRole($boardRelation)
1375 + {
1376 + return $boardRelation && Arr::get($boardRelation->settings, 'is_admin')
1377 + ? 'manager'
1378 + : ($boardRelation && Arr::has($boardRelation->settings, 'is_viewer_only') && Arr::get($boardRelation->settings, 'is_viewer_only')
1379 + ? 'viewer'
1380 + : 'member');
1381 + }
1382 + public function uploadBoardBackground(Request $request,$board_id)
1383 + {
1384 + $file = Arr::get($request->files(), 'file')->toArray();
1385 + (new \FluentBoards\App\Services\UploadService)->validateFile($file);
1386 +
1387 + $uploadInfo = UploadService::handleFileUpload( $request->files(), $board_id);
1388 +
1389 + $fileData = $uploadInfo[0];
1390 + $initialDataData = [
1391 + 'type' => 'url',
1392 + 'url' => '',
1393 + 'name' => '',
1394 + 'size' => 0,
1395 + ];
1396 +
1397 + $attachData = array_merge($initialDataData, $fileData);
1398 + $UrlMeta = [];
1399 + if($attachData['type'] == 'url') {
1400 + $UrlMeta = RemoteUrlParser::parse($attachData['url']);
1401 + }
1402 + $uid = wp_generate_uuid4();
1403 + $fileUploadedData = new Attachment();
1404 + $fileUploadedData->object_id = $board_id;
1405 + $fileUploadedData->object_type = Constant::BOARD_BACKGROUND_IMAGE;
1406 + $fileUploadedData->attachment_type = $attachData['type'];
1407 + $fileUploadedData->title = (new TaskService())->setTitle($attachData['type'], $attachData['name'], $UrlMeta);
1408 + $fileUploadedData->file_path = $attachData['type'] != 'url' ? $attachData['file'] : null;
1409 + $fileUploadedData->full_url = esc_url($attachData['url']);
1410 + $fileUploadedData->file_size = $attachData['size'];
1411 + $fileUploadedData->settings = $attachData['type'] == 'url' ? [
1412 + 'meta' => $UrlMeta
1413 + ] : '';
1414 + $fileUploadedData->driver = 'local';
1415 + $fileUploadedData->file_hash = md5($uid . wp_rand(0, 1000));
1416 + $fileUploadedData->save();
1417 + if(!!defined('FLUENT_BOARDS_PRO_VERSION')) {
1418 + $mediaData = (new AttachmentService())->processMediaData($fileData, $file);
1419 + $fileUploadedData['driver'] = $mediaData['driver'];
1420 + $fileUploadedData['file_path'] = $mediaData['file_path'];
1421 + $fileUploadedData['full_url'] = $mediaData['full_url'];
1422 + $fileUploadedData->save();
1423 + }
1424 +
1425 + $board = Board::find($board_id);
1426 + $oldBackground = $board->background;
1427 + $publicUrl = (new CommentService())->createPublicUrl($fileUploadedData, $board_id);
1428 + $background = [
1429 + 'color' => null,
1430 + 'id' => $fileUploadedData->id,
1431 + 'image_url' => $publicUrl,
1432 + 'is_image' => true,
1433 + ];
1434 + $board->background = $background;
1435 + $board->save();
1436 + do_action('fluent_boards/board_background_updated', $board_id, $oldBackground);
1437 +
1438 + return $this->sendSuccess([
1439 + 'message' => __('Background updated successfully', 'fluent-boards'),
1440 + 'background' => $board->background,
1441 + ]);
1442 + }
1443 +
1444 + public function getPinnedBoards()
1445 + {
1446 + $pinnedBoards = $this->boardService->getPinnedBoards();
1447 +
1448 + return $this->sendSuccess([
1449 + 'pinnedBoards' => $pinnedBoards,
1450 + ], 200);
1451 + }
1452 +
1453 + public function pinBoard($boardId)
1454 + {
1455 + $this->boardService->pinBoard($boardId);
1456 +
1457 + return $this->sendSuccess([
1458 + 'message' => __('The Board has been pinned', 'fluent-boards'),
1459 + ], 200);
1460 + }
1461 +
1462 + public function unpinBoard($boardId)
1463 + {
1464 + $remove = $this->boardService->unpinBoard($boardId);
1465 +
1466 + if (!$remove) {
1467 + return $this->sendError([
1468 + 'message' => __('Board is not pinned', 'fluent-boards'),
1469 + ], 400);
1470 + }
1471 +
1472 + return $this->sendSuccess([
1473 + 'message' => __('Board is removed from pinned boards', 'fluent-boards'),
1474 + ], 200);
1475 + }
1476 +
1477 + public function getBoardFolder($board_id)
1478 + {
1479 + try {
1480 + $folder = $this->boardService->getBoardFolder($board_id);
1481 + return $this->sendSuccess([
1482 + 'folder' => $folder,
1483 + ], 200);
1484 + } catch (\Exception $e) {
1485 + return $this->sendError($e->getMessage(), 400);
1486 + }
1487 + }
1488 +
1489 + public function getBoardMenuItems($board_id)
1490 + {
1491 + try {
1492 + $menuItems = (new BoardMenuHandler())->getMenuItems($board_id);
1493 +
1494 + return $this->sendSuccess([
1495 + 'menu_items' => $menuItems
1496 + ], 200);
1497 + } catch (\Exception $e) {
1498 + return $this->sendError($e->getMessage(), 500);
1499 + }
1500 + }
1501 +
1502 + public function getPublicAccessSettings($board_id)
1503 + {
1504 + $board_id = absint($board_id);
1505 + $board = Board::findOrFail($board_id);
1506 +
1507 + $enabled = (bool) $board->getMetaByKey('public_access_enabled');
1508 + $shortcode = $enabled ? '[fluent_board_public id="' . $board_id . '"]' : '';
1509 +
1510 + return $this->sendSuccess([
1511 + 'enabled' => $enabled,
1512 + 'shortcode' => $shortcode,
1513 + ], 200);
1514 + }
1515 +
1516 + public function togglePublicAccess(Request $request, $board_id)
1517 + {
1518 + $board_id = absint($board_id);
1519 + $board = Board::findOrFail($board_id);
1520 +
1521 + $enabled = filter_var(
1522 + $request->getSafe('enabled', 'sanitize_text_field', false),
1523 + FILTER_VALIDATE_BOOLEAN
1524 + );
1525 +
1526 + $board->updateMeta('public_access_enabled', $enabled ? '1' : '');
1527 +
1528 + $shortcode = $enabled ? '[fluent_board_public id="' . $board_id . '"]' : '';
1529 +
1530 + return $this->sendSuccess([
1531 + 'message' => $enabled
1532 + ? __('Public access has been enabled', 'fluent-boards')
1533 + : __('Public access has been disabled', 'fluent-boards'),
1534 + 'enabled' => $enabled,
1535 + 'shortcode' => $shortcode,
1536 + ], 200);
1537 + }
1538 +
1539 + /**
1540 + * Resolve a stage only when it belongs to the requested board.
1541 + *
1542 + * @param int $stageId
1543 + * @param int $boardId
1544 + * @return Stage
1545 + * @throws \Exception
1546 + */
1547 + private function findStageOnBoard($stageId, $boardId)
1548 + {
1549 + $stage = Stage::where('id', absint($stageId))
1550 + ->where('board_id', absint($boardId))
1551 + ->first();
1552 +
1553 + if (!$stage) {
1554 + throw new \Exception(esc_html__('Stage not found', 'fluent-boards'));
1555 + }
1556 +
1557 + return $stage;
896 1558 }
897 1559
898 1560 }