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