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