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 +814 -173 1.202.1.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,9 +55,10 @@
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');
@@ -53,10 +64,48 @@
53 64 $order = $request->getSafe('order', 'sanitize_text_field', 'created_at');
54 65 $orderBy = $request->getSafe('orderBy', 'sanitize_text_field', 'DESC');
55 66 $searchInput = $request->getSafe('searchInput', 'sanitize_text_field');
56 67
57 - $relatedBoardsQuery = Board::where('type', $type)
58 - ->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
59 108 if (!empty($searchInput)) {
60 109 $relatedBoardsQuery = $relatedBoardsQuery->where('title', 'like', '%' . $searchInput . '%');
61 110 }
62 111
@@ -65,37 +114,114 @@
65 114 ->with('stages', 'users')
66 115 ->paginate($per_page);
67 116
68 117 foreach ($relatedBoards as $relatedBoard) {
118 + $relatedBoard->description = DescriptionMarkdownConverter::normalize($relatedBoard->description);
69 119 $relatedBoard->users = Helper::sanitizeUserCollections($relatedBoard->users);
120 + $relatedBoard->is_pinned = $this->boardService->isPinned($relatedBoard->id);
70 121 }
71 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 +
72 191 return $this->sendSuccess([
73 - 'boards' => $relatedBoards
192 + 'boards' => $relatedBoards,
193 + 'all_stages' => $stages,
74 194 ], 200);
75 195 }
76 -
77 - public function getBoardsList(Request $request)
196 + public function getOnlyBoardsByUser(Request $request)
78 197 {
79 198 try {
80 199 $userId = get_current_user_id();
81 200
82 - if (PermissionManager::isAdmin($userId)) {
83 - $relatedBoardsQuery = Board::query();
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();
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,9 +228,12 @@
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)
108 237 ->withCount('completedTasks')
109 238 ->with(['stages', 'users'])
110 239 ->get();
@@ -111,8 +240,9 @@
111 240 }
112 241
113 242 foreach ($boards as $board) {
114 243 $board->users = Helper::sanitizeUserCollections($board->users);
244 + $board->is_pinned = $this->boardService->isPinned($board->id);
115 245 }
116 246
117 247 return [
118 248 'boards' => $boards,
@@ -118,45 +248,11 @@
118 248 'boards' => $boards,
119 249 ];
120 250 }
121 251
122 - public function getBoardMeta($board_id)
123 - {
124 - $boards = $this->boardService->fetchBoardMeta($board_id);
125 -
126 - return $this->sendSuccess([
127 - 'boards' => $boards,
128 - ], 200);
129 - }
130 -
131 - public function setAuthenticationPermission(Request $request, $board_id)
132 - {
133 - $boardData = $this->boardSanitizeAndValidate($request->only([
134 - 'is_auth_require_idea_submit',
135 - 'is_auth_require_voting_commenting',
136 - 'is_auth_require_reaction',
137 - 'is_allow_email_along_with_auth',
138 - 'is_allow_unauthentication_reaction_along_with_auth'
139 - ]), [
140 - 'is_auth_require_idea_submit' => 'required',
141 - 'is_auth_require_voting_commenting' => 'required',
142 - 'is_auth_require_reaction' => 'required',
143 - 'is_allow_email_along_with_auth' => 'nullable',
144 - 'is_allow_unauthentication_reaction_along_with_auth' => 'nullable'
145 - ]);
146 -
147 - try {
148 - $boards = $this->boardService->modifyAuthenticationPermission($boardData, $board_id);
149 -
150 - return $this->sendSuccess([
151 - 'message' => __("Board has been updated", 'fluent-boards'),
152 - 'boards' => $boards,
153 - ], 200);
154 - } catch (\Exception $e) {
155 - return $this->sendError($e->getMessage(), 404);
156 - }
157 - }
158 -
252 + /*
253 + * TODO: Refactor this method , remove this
254 + */
159 255 public function getBoardsByType($type)
160 256 {
161 257 $boards = $this->boardService->getBoardsByType($type);
162 258
@@ -161,9 +257,9 @@
161 257 $boards = $this->boardService->getBoardsByType($type);
162 258
163 259 return $this->sendSuccess([
164 260 'boards' => $boards,
165 - ], 200);
261 + ]);
166 262 }
167 263
168 264 public function createFirstBoard(Request $request)
169 265 {
@@ -174,11 +270,14 @@
174 270 'currency' => 'nullable|string',
175 271 'crm_contact_id' => 'nullable|numeric',
176 272 ]);
177 273
178 - $installFluentCRM = $request->get('withFluentCRM') == 'yes' ? true : false;
274 + $installFluentCRM = $request->getSafe('withFluentCRM', 'sanitize_text_field') == 'yes' ? true : false;
179 275
180 276 $postStages = $request->get('stages');
277 + if (!is_array($postStages)) {
278 + $postStages = [];
279 + }
181 280 $stageData = array();
182 281 foreach ($postStages as $stage) {
183 282 $temp = $this->stageSanitizeAndValidate($stage, [
184 283 'title' => 'required|string',
@@ -188,9 +287,9 @@
188 287
189 288 $taskData = null;
190 289 if ($request->get('task')) {
191 290 $taskData = $this->taskSanitizeAndValidate($request->get('task'), [
192 - 'title' => 'required|string'
291 + 'title' => 'required|string',
193 292 ]);
194 293 }
195 294
196 295 $board = $this->boardService->createBoard($boardData);
@@ -204,9 +303,9 @@
204 303 $this->taskService->createTask($taskData, $board->id);
205 304 }
206 305
207 306 do_action('fluent_boards/board_created', $board);
208 -
307 +
209 308 if ($installFluentCRM && !defined('FLUENTCRM')) {
210 309 InstallService::install('fluent-crm');
211 310 }
212 311
@@ -215,8 +314,21 @@
215 314 'board' => $board,
216 315 ];
217 316 }
218 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 +
219 331 public function create(Request $request)
220 332 {
221 333 $boardData = $this->boardSanitizeAndValidate($request->get('board'), [
222 334 'title' => 'required|string',
@@ -223,17 +335,46 @@
223 335 'description' => 'nullable',
224 336 'type' => 'required|string',
225 337 'currency' => 'nullable|string',
226 338 'crm_contact_id' => 'nullable|numeric',
339 + 'folder_id' => 'nullable',
227 340 ]);
228 341
229 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 +
230 356 $board = $this->boardService->createBoard($boardData);
231 - $this->labelService->createDefaultLabel($board->id);
357 + $this->createBoardLabelsFromRequest($request, $board->id);
358 + $this->addBoardMembersFromRequest($request, $board->id);
232 359 $type = ucfirst($boardData['type']);
360 + $stages = $request->get('stages');
361 + $sanitizedStages = [];
233 362
234 - if (isset($boardData['is_roadmap']) && $boardData['is_roadmap'] == 'yes') {
235 - $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);
236 377 } else {
237 378 $this->stageService->createDefaultStages($board);
238 379 }
239 380
@@ -243,37 +384,151 @@
243 384 }
244 385
245 386 do_action('fluent_boards/board_created', $board);
246 387
388 +
389 + if ($folderId) {
390 + $folderService->addBoardToFolder($folderId, [$board->id]);
391 + }
392 +
247 393 $message = __('Board has been created successfully', 'fluent-boards');
248 394
249 - return $this->sendSuccess([
395 + return $this->send([
250 396 'message' => $message,
251 397 'board' => $board,
252 398 ], 201);
253 399 } catch (\Exception $e) {
254 - return $this->sendError($e->getMessage(), 400);
400 + return $this->sendError([
401 + 'message' => $e->getMessage()
402 + ]);
255 403 }
256 404 }
257 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 + */
258 519 public function getArchivedStage(Request $request, $board_id)
259 520 {
260 521 try {
261 - $pagination = $request->noPagination ? true : false;
262 - $per_page = isset($data['per_page']) ? $data['per_page'] : 30;
263 - $page = isset($data['page']) ? $data['page'] : 1;
264 - if ($pagination) {
265 - $stages = Stage::where('board_id', $board_id)
266 - ->whereNotNull('archived_at')
267 - ->orderBy('created_at', 'DESC')
268 - ->get();
269 - } else {
270 - $stages = Stage::where('board_id', $board_id)
271 - ->whereNotNull('archived_at')
272 - ->orderBy('created_at', 'DESC')
273 - ->paginate($per_page, ['*'], 'page', $page);
274 - }
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 + ];
275 528
529 + $stages = $this->stageService->getArchivedStages($sanitizedParams, $board_id);
530 +
276 531 return $this->sendSuccess([
277 532 'stages' => $stages,
278 533 ], 200);
279 534 } catch (\Exception $e) {
@@ -280,17 +535,33 @@
280 535 return $this->sendError($e->getMessage(), 404);
281 536 }
282 537 }
283 538
284 - public function find($board_id)
539 + public function find(Request $request, $board_id)
285 540 {
286 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);
287 544 $board->background = maybe_unserialize($board->background);
288 545 $board->createdOn = $board->created_at->format('Y-m-d');
289 546
290 - $board->load(['users', 'stages', 'labels', 'owner']);
547 + $board->load(['users', 'labels', 'owner']);
291 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 +
292 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 +
293 564 $board->load(['customFields']);
294 565 }
295 566
296 567 $this->boardService->updateRecentBoards($board_id);
@@ -296,21 +567,35 @@
296 567 $this->boardService->updateRecentBoards($board_id);
297 568
298 569 $board->labelColor = Constant::TRELLO_COLOR_MAP;
299 570 $board->labelColorText = Constant::TEXT_COLOR_MAP;
571 + $board->labelColorPresets = Constant::LABEL_COLOR_PRESETS;
300 572
301 573 $board->users = Helper::sanitizeUserCollections($board->users);
302 574 $board->owner = Helper::sanitizeUserCollections($board->owner);
303 575
576 + $board->is_pinned = $this->boardService->isPinned($board->id);
577 +
304 578 $board = apply_filters('fluent_boards/board_find', $board);
305 579
306 580 return [
307 - 'board' => $board
581 + 'board' => $board,
582 + 'synced_at' => current_time('mysql')
308 583 ];
309 584 }
310 585
311 586 public function update(Request $request, $board_id)
312 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 +
313 598 $boardData = $this->boardSanitizeAndValidate($request->only(['title', 'description']), [
314 599 'title' => 'required|string',
315 600 'description' => 'nullable|string',
316 601 ]);
@@ -315,8 +600,9 @@
315 600 'description' => 'nullable|string',
316 601 ]);
317 602
318 603 $board = Board::findOrFail($board_id);
604 + $boardData['description'] = DescriptionMarkdownConverter::normalize($boardData['description']);
319 605
320 606 $oldBoard = clone $board;
321 607 $board->fill($boardData);
322 608 $board->save();
@@ -323,21 +609,23 @@
323 609
324 610 do_action('fluent_boards/board_updated', $board, $oldBoard);
325 611
326 612 return [
327 - 'stages' => $board->stages()->get(),
328 613 'message' => __('Board has been updated', 'fluent-boards'),
329 614 'board' => $board,
615 + 'stages' => $board->stages()->get(),
330 616 ];
331 617 }
332 618
333 619 public function archiveStage($board_id, $stage_id)
334 620 {
621 + $board_id = absint($board_id);
622 + $stage_id = absint($stage_id);
623 +
335 624 try {
336 - $stage = Stage::findOrFail($stage_id);
337 - $board = Board::findOrFail($stage->board_id);
625 + $stage = $this->findStageOnBoard($stage_id, $board_id);
338 626
339 - $updatedStage = $this->boardService->archiveStage($board->id, $stage);
627 + $updatedStage = $this->boardService->archiveStage($board_id, $stage);
340 628
341 629 return $this->sendSuccess([
342 630 'updatedStage' => $updatedStage,
343 631 'message' => __('Stage has been archived', 'fluent-boards'),
@@ -348,18 +636,20 @@
348 636 }
349 637
350 638 public function restoreStage($board_id, $stage_id)
351 639 {
640 + $board_id = absint($board_id);
641 + $stage_id = absint($stage_id);
642 +
352 643 try {
353 - $stage = Stage::findOrFail($stage_id);
354 - $board = Board::findOrFail($board_id);
644 + $stage = $this->findStageOnBoard($stage_id, $board_id);
355 645
356 - $updatedStage = $this->boardService->restoreStage($board->id, $stage);
646 + $updatedStage = $this->boardService->restoreStage($board_id, $stage);
357 647
358 648 return $this->sendSuccess([
359 - 'success' => true,
649 + 'success' => true,
360 650 'updatedStage' => $updatedStage,
361 - 'message' => __('Stage has been restored', 'fluent-boards')
651 + 'message' => __('Stage has been restored', 'fluent-boards')
362 652 ], 200);
363 653 } catch (\Exception $e) {
364 654 return $this->sendError($e->getMessage(), 400);
365 655 }
@@ -365,32 +655,22 @@
365 655 }
366 656 }
367 657
368 658
369 - public function changePositionOfStage(Request $request, $board_id)
659 + public function repositionStages(Request $request, $board_id)
370 660 {
371 - $changeData = $this->boardSanitizeAndValidate($request->only(['fromPosition', 'toPosition']), [
372 - 'fromPosition' => 'required',
373 - 'toPosition' => 'required',
374 - ]);
375 -
661 + $incomingList = $request->get('list');
662 + if (!is_array($incomingList)) {
663 + $incomingList = [];
664 + }
665 + $incomingList = array_map('intval', $incomingList);
376 666 try {
377 - $this->boardService->changePositionOfStage($board_id, $changeData);
667 + foreach ($incomingList as $stageId) {
668 + $this->findStageOnBoard($stageId, $board_id);
669 + }
378 670
671 + $this->boardService->repositionStages($board_id, $incomingList);
379 672 return $this->sendSuccess([
380 - 'message' => __('Board stage has been updated', 'fluent-boards')
381 - ], 200);
382 - } catch (\Exception $e) {
383 - return $this->sendError($e->getMessage(), 400);
384 - }
385 - }
386 -
387 - public function rePositionStages(Request $request, $board_id)
388 - {
389 - $incomingList = $request->get('list');
390 - try {
391 - $this->boardService->rePositionStages($board_id, $incomingList);
392 - return $this->sendSuccess([
393 673 'message' => __('Stages Reordered', 'fluent-boards'),
394 674 'updatedStages' => $this->stageService->getLastOneMinuteUpdatedStages($board_id)
395 675 ], 200);
396 676 } catch (\Exception $e) {
@@ -408,9 +688,9 @@
408 688 public function delete($board_id)
409 689 {
410 690 try {
411 691 if (!PermissionManager::isAdmin()) {
412 - 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);
413 693 }
414 694 $this->boardService->deleteBoard($board_id);
415 695
416 696 return $this->sendSuccess([
@@ -428,9 +708,12 @@
428 708
429 709 public function getActivities(Request $request, $board_id)
430 710 {
431 711 try {
432 - $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 + ]);
433 716 return $this->sendSuccess([
434 717 'activities' => $activities,
435 718 ], 200);
436 719 } catch (\Exception $e) {
@@ -437,8 +720,11 @@
437 720 return $this->sendError($e->getMessage(), 404);
438 721 }
439 722 }
440 723
724 + /*
725 + * TODO: Refactor this method - for Masiur
726 + */
441 727 public function getBoardUsers($board_id)
442 728 {
443 729 $board = Board::findOrFail($board_id);
444 730
@@ -445,8 +731,11 @@
445 731 $boardObjects = Relation::where('object_type', 'board_user')
446 732 ->where('object_id', $board_id)
447 733 ->get()->keyBy('foreign_id');
448 734
735 + $superAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
736 + ->get()->pluck('object_id')->toArray();
737 +
449 738 $userIds = $boardObjects->pluck('foreign_id')->toArray();
450 739
451 740 $coreUsers = [];
452 741 if ($userIds) {
@@ -465,14 +754,17 @@
465 754 }
466 755
467 756 $boardRelation = $boardObjects[$user->ID] ?? null;
468 757
758 +
469 759 $formattedUsers[] = [
470 760 'ID' => $user->ID,
471 761 'display_name' => $name,
472 762 'email' => $user->user_email,
473 763 'photo' => fluent_boards_user_avatar($user->user_email, $name),
474 - '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')
475 767 ];
476 768 }
477 769
478 770 // order formatted users by display_name
@@ -480,9 +772,9 @@
480 772 return strcmp($a['display_name'], $b['display_name']);
481 773 });
482 774
483 775 $returnData = [
484 - 'users' => Helper::sanitizeUsersArray($formattedUsers),
776 + 'users' => Helper::sanitizeUsersArray($formattedUsers, $board_id),
485 777 'global_admins' => []
486 778 ];
487 779
488 780 if (!PermissionManager::isAdmin(get_current_user_id())) {
@@ -489,16 +781,24 @@
489 781 return $returnData;
490 782 }
491 783
492 784 /*
493 - * 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.
494 786 */
495 - $adminUserIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
787 + $fluentBoardAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
496 788 ->whereNotIn('object_id', $userIds)
497 789 ->get()
498 790 ->pluck('object_id')
499 791 ->toArray();
500 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 +
501 801 if ($adminUserIds) {
502 802 $adminUsers = get_users([
503 803 'include' => $adminUserIds,
504 804 ]);
@@ -515,9 +815,11 @@
515 815 'ID' => $user->ID,
516 816 'display_name' => $name,
517 817 'email' => $user->user_email,
518 818 'photo' => fluent_boards_user_avatar($user->user_email, $name),
519 - 'role' => 'admin'
819 + 'role' => 'admin',
820 + 'is_super' => in_array($user->ID, $superAdminIds),
821 + 'is_wpadmin' => $user->has_cap('manage_options')
520 822 ];
521 823 }
522 824
523 825 // order formatted users by display_name
@@ -524,9 +826,9 @@
524 826 usort($formattedAdminUsers, function ($a, $b) {
525 827 return strcmp($a['display_name'], $b['display_name']);
526 828 });
527 829
528 - $returnData['global_admins'] = Helper::sanitizeUsersArray($formattedAdminUsers);
830 + $returnData['global_admins'] = Helper::sanitizeUsersArray($formattedAdminUsers, $board_id);
529 831 }
530 832
531 833 return $this->sendSuccess($returnData, 200);
532 834 }
@@ -546,18 +848,25 @@
546 848 }
547 849
548 850 public function addMembersInBoard(Request $request, $board_id)
549 851 {
550 - $memberId = $request->getSafe('memberId');
551 - $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);
552 855
553 - if ($isAlreadyMember) {
856 + if ($member === null) {
554 857 return $this->sendError([
858 + 'message' => __('User not found.', 'fluent-boards'),
859 + ], 404);
860 + }
861 +
862 + if (!$member) {
863 + return $this->sendError([
555 864 'message' => __('User already a member', 'fluent-boards'),
556 - ], 304);
865 + ], 409);
557 866 }
558 - $member = $this->boardService->addMembersInBoard($board_id, $memberId);
559 867
868 +
560 869 return [
561 870 'message' => __('Member added successfully', 'fluent-boards'),
562 871 'member' => Helper::sanitizeUserCollections($member)
563 872 ];
@@ -585,11 +894,11 @@
585 894 }
586 895
587 896 public function searchBoards(Request $request)
588 897 {
589 - $per_page = $request->get('per_page', 10);
590 - $search_input = $request->searchInput . trim('');
591 - $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');
592 901
593 902 $currentUserId = get_current_user_id();
594 903
595 904 if (PermissionManager::isAdmin($currentUserId)) {
@@ -631,10 +940,13 @@
631 940 * @return
632 941 */
633 942 public function changeStageView($board_id, $stage_id)
634 943 {
944 + $board_id = absint($board_id);
945 + $stage_id = absint($stage_id);
946 +
635 947 try {
636 - $stage = Stage::findOrFail($stage_id);
948 + $stage = $this->findStageOnBoard($stage_id, $board_id);
637 949 $message = __('The stage is made public!', 'fluent-boards');
638 950 $settings = $stage->settings;
639 951
640 952 if (isset($settings['is_public'])) {
@@ -639,9 +951,9 @@
639 951
640 952 if (isset($settings['is_public'])) {
641 953 if ($settings['is_public']) {
642 954 $settings['is_public'] = false;
643 - $message = __('The stage is made admin only!', 'fluent-boards');
955 + $message = __('The stage is made private!', 'fluent-boards');
644 956 } else {
645 957 $settings['is_public'] = true;
646 958 }
647 959 } else {
@@ -660,24 +972,27 @@
660 972 }
661 973
662 974
663 975 /**
664 - * Set board background image or color
976 + * Set or reset board background image/color.
665 977 * @param \FluentBoards\Framework\Http\Request\Request $request
666 978 * @return
667 979 */
668 980 public function setBoardBackground(Request $request, $board_id)
669 981 {
670 - // sanitize and validate image_url
671 - 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) {
672 990 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
673 - "id" => 'required',
991 + 'id' => 'required|integer',
674 992 'image_url' => 'required|string|url',
675 993 ]);
676 - }
677 -
678 - // sanitize and validate color
679 - if ($request->color) {
994 + } elseif ($request->color) {
680 995 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
681 996 "id" => 'required',
682 997 'color' => 'required',
683 998 ]);
@@ -685,17 +1000,22 @@
685 1000
686 1001 try {
687 1002 if (!$board_id) {
688 1003 $errorMessage = __('Board id is required', 'fluent-boards');
689 - throw new \Exception($errorMessage, 400);
1004 + throw new \Exception(esc_html($errorMessage), 400);
690 1005 }
691 1006
1007 + if (empty($backgroundData)) {
1008 + $errorMessage = __('Background data is required', 'fluent-boards');
1009 + throw new \Exception(esc_html($errorMessage), 400);
1010 + }
1011 +
692 1012 return $this->sendSuccess([
693 1013 'message' => __('Background updated successfully', 'fluent-boards'),
694 1014 'background' => $this->boardService->setBoardBackground($backgroundData, $board_id),
695 1015 ]);
696 1016 } catch (\Exception $e) {
697 - $this->sendError([$e->getMessage(), 400]);
1017 + return $this->sendError($e->getMessage(), 400);
698 1018 }
699 1019 }
700 1020
701 1021
@@ -705,15 +1025,19 @@
705 1025 * @param mixed $stage_slug
706 1026 * @return $availablePositions as an array
707 1027 * @throws \Exception
708 1028 */
709 - public function getStageTaskAvailablePositions($board_id, $stage_id)
1029 + public function getStageTaskAvailablePositions(Request $request, $board_id, $stage_id)
710 1030 {
711 1031 try {
712 1032 if ($board_id && $stage_id) {
713 - $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);
714 1035 return $this->sendSuccess([
715 - 'availablePositions' => $availablePositions
1036 + 'availablePositions' => $availablePositions['availablePositions'],
1037 + 'moveTargets' => $availablePositions['moveTargets'],
1038 + 'currentMoveTargetKey' => $availablePositions['currentMoveTargetKey'],
1039 + 'defaultMoveTargetKey' => $availablePositions['defaultMoveTargetKey'],
716 1040 ], 200);
717 1041 } else {
718 1042 $message = '';
719 1043 if (!$board_id) {
@@ -721,12 +1045,12 @@
721 1045 }
722 1046 if (!$stage_id) {
723 1047 $message = 'Stage ';
724 1048 }
725 - throw new \Exception($message . 'is required', 400);
1049 + throw new \Exception(esc_html($message . 'is required'), 400);
726 1050 }
727 1051 } catch (\Exception $e) {
728 - $this->sendError([$e->getMessage(), 400]);
1052 + return $this->sendError($e->getMessage(), 400);
729 1053 }
730 1054 }
731 1055
732 1056 public function getAssociateCrmContacts($board_id)
@@ -735,24 +1059,47 @@
735 1059 $contactAssociatedTasks = Task::with('board')->where('board_id', $board_id)
736 1060 ->whereNotNull('crm_contact_id')
737 1061 ->get();
738 1062
739 - $formattedContacts = Collection::make($contactAssociatedTasks)
740 - ->groupBy('crm_contact_id')
741 - ->map(function ($tasks, $contactId) {
742 - $subscriber = Subscriber::find($contactId);
743 - 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) {
744 1092 return null; // Skip if subscriber not found
745 1093 }
746 1094
747 - return [
748 - 'name' => $subscriber->first_name . ' ' . $subscriber->last_name,
749 - 'photo' => $subscriber->photo,
750 - 'email' => $subscriber->email,
751 - 'crm_contact_id' => $contactId,
752 - 'id' => $contactId,
753 - 'tasks' => $tasks,
754 - ];
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;
755 1102 })
756 1103 ->filter()->toArray();
757 1104
758 1105
@@ -773,11 +1120,13 @@
773 1120 'message' => __('Associated Crm Member has been updated', 'fluent-boards'),
774 1121 ], 200);
775 1122 }
776 1123
777 - public function hasDataChanged($board_id)
1124 + public function hasDataChanged(Request $request, $board_id)
778 1125 {
779 - 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);
780 1129 }
781 1130
782 1131 public function createStage(Request $request, $board_id)
783 1132 {
@@ -782,8 +1131,9 @@
782 1131 public function createStage(Request $request, $board_id)
783 1132 {
784 1133 $stageData = $this->stageSanitizeAndValidate($request->all(), [
785 1134 'title' => 'required|string',
1135 + 'position' => 'nullable|numeric'
786 1136 ]);
787 1137
788 1138 $board = Board::find($board_id);
789 1139 $stage = $this->stageService->createStage($stageData, $board_id);
@@ -799,15 +1149,27 @@
799 1149 }
800 1150
801 1151 public function moveAllTasks(Request $request, $board_id)
802 1152 {
803 - $oldStageId = $request->getSafe('oldStageId');
804 - $newStageId = $request->getSafe('newStageId');
1153 + $oldStageId = $request->getSafe('oldStageId', 'intval');
1154 + $newStageId = $request->getSafe('newStageId', 'intval');
805 1155
1156 + if (!$oldStageId || !$newStageId) {
1157 + return $this->sendError(__('Invalid stage IDs provided', 'fluent-boards'), 400);
1158 + }
1159 +
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();
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 +
806 1168 $updates = $this->stageService->moveAllTasks($oldStageId, $newStageId, $board_id);
807 1169
808 1170 return [
809 - 'message' => __('Tasks has been Moved', 'fluent-boards'),
1171 + 'message' => __('Tasks have been moved', 'fluent-boards'),
810 1172 'updatedTasks' => $updates,
811 1173 ];
812 1174
813 1175 }
@@ -813,50 +1175,87 @@
813 1175 }
814 1176
815 1177 public function archiveAllTasksInStage($board_id, $stage_id)
816 1178 {
817 - $updates = $this->stageService->archiveAllTasksInStage($stage_id);
818 - return [
819 - 'message' => __('Tasks has been archived', 'fluent-boards'),
820 - 'updatedTasks' => $updates,
821 - ];
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 + }
822 1193 }
823 1194
824 1195 public function getAssociatedBoards(Request $request, $associated_id)
825 1196 {
826 - $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 +
827 1209 return [
828 1210 'boards' => $associatedBoards,
829 1211 ];
830 1212 }
831 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 +
832 1225 public function duplicateBoard(Request $request, $board_id)
833 1226 {
834 1227 $boardData = $this->taskSanitizeAndValidate($request->get('board'), [
835 1228 'title' => 'required|string'
836 1229 ]);
1230 +
1231 + $boardData['source_board_id'] = $board_id;
1232 +
837 1233 $isWithLabels = $request->getSafe('isWithLabels');
838 1234 $isWithTasks = $request->getSafe('isWithTasks');
1235 + $isWithTemplates = $request->getSafe('isWithTemplates');
839 1236
840 1237 try {
841 1238 if(!PermissionManager::isAdmin()) {
842 1239 $errorMessage = __('You do not have permission to duplicate board', 'fluent-boards');
843 - throw new \Exception($errorMessage, 400);
1240 + throw new \Exception(esc_html($errorMessage), 400);
844 1241 }
845 1242 //create board
846 1243 $newBoard = $this->boardService->copyBoard($boardData);
847 1244
848 1245 //label copy
1246 + $labelMap = [];
1247 +
849 1248 if ($isWithLabels == 'yes') {
850 - $this->labelService->copyLabelsOfBoard($board_id, $newBoard);
1249 + $labelMap = $this->labelService->copyLabelsOfBoard($board_id, $newBoard);
851 1250 }
852 1251
853 1252 //stage copy
854 - $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id);
1253 + $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id, $isWithTemplates);
855 1254
856 1255 //copy tasks of selected stages
857 1256 if ($isWithTasks == 'yes') {
858 - $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard);
1257 + $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap,$isWithTemplates);
859 1258 }
860 1259
861 1260 return $this->sendSuccess([
862 1261 'board' => $newBoard,
@@ -868,11 +1267,18 @@
868 1267
869 1268 public function importFromBoard(Request $request, $board_id)
870 1269 {
871 1270 $selectedStages = $request->getSafe('selectedStages');
1271 + $position = $request->getSafe('position', 'intval');
872 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 +
873 1279 try {
874 - $this->stageService->importStagesFromBoard($board_id, $selectedStages);
1280 + $this->stageService->importStagesFromBoard($board_id, $selectedStages, $position);
875 1281
876 1282 return $this->sendSuccess([
877 1283 'message' => __('Import successfully', 'fluent-boards'),
878 1284 ], 200);
@@ -887,7 +1293,242 @@
887 1293 return [
888 1294 'solidColors' => Constant::BOARD_BACKGROUND_DEFAULT_SOLID_COLORS,
889 1295 'gradients' => Constant::BOARD_BACKGROUND_DEFAULT_GRADIENT_COLORS
890 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;
891 1532 }
892 1533
893 1534 }