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