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 +841 -174 1.20 → 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,41 +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 209
82 - if (PermissionManager::isAdmin($userId)) {
83 - $relatedBoardsQuery = Board::query();
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);
84 216 } else {
85 - $currentUser = User::find($userId);
86 - $relatedBoardsQuery = $currentUser->whichBoards();
217 + $relatedBoardsQuery = Board::whereNull('archived_at')->excludeTemplates()->byAccessUser($userId);
87 218 }
88 219
89 - $relatedBoards = $relatedBoardsQuery->with('stages')->get();
90 - $stages = Stage::whereIn('board_id', $relatedBoards->pluck('id'))->get();
220 + if (!empty($searchInput)) {
221 + $relatedBoardsQuery = $relatedBoardsQuery->where('title', 'like', '%' . $searchInput . '%');
222 + }
91 223
224 + $relatedBoards = $relatedBoardsQuery->orderBy('created_at', 'DESC')->get();
225 +
92 226 return $this->sendSuccess([
93 - 'boards' => $relatedBoards,
94 - 'all_stages' => $stages,
95 - ], 200);
227 + 'boards' => $relatedBoards
228 + ]);
96 229 } catch (\Exception $e) {
97 - return $this->sendError($e->getMessage(), 404);
230 + return $this->sendError([
231 + 'message' => $e->getMessage()
232 + ]);
98 233 }
99 234 }
100 235
101 236 public function getRecentBoards()
@@ -102,9 +237,12 @@
102 237 {
103 238 $boards = $this->boardService->getRecentBoards();
104 239
105 240 if (!$boards || $boards->isEmpty()) {
106 - $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())
107 245 ->limit(4)
108 246 ->withCount('completedTasks')
109 247 ->with(['stages', 'users'])
110 248 ->get();
@@ -111,8 +249,9 @@
111 249 }
112 250
113 251 foreach ($boards as $board) {
114 252 $board->users = Helper::sanitizeUserCollections($board->users);
253 + $board->is_pinned = $this->boardService->isPinned($board->id);
115 254 }
116 255
117 256 return [
118 257 'boards' => $boards,
@@ -118,45 +257,11 @@
118 257 'boards' => $boards,
119 258 ];
120 259 }
121 260
122 - public function getBoardMeta($board_id)
123 - {
124 - $boards = $this->boardService->fetchBoardMeta($board_id);
125 -
126 - return $this->sendSuccess([
127 - 'boards' => $boards,
128 - ], 200);
129 - }
130 -
131 - public function setAuthenticationPermission(Request $request, $board_id)
132 - {
133 - $boardData = $this->boardSanitizeAndValidate($request->only([
134 - 'is_auth_require_idea_submit',
135 - 'is_auth_require_voting_commenting',
136 - 'is_auth_require_reaction',
137 - 'is_allow_email_along_with_auth',
138 - 'is_allow_unauthentication_reaction_along_with_auth'
139 - ]), [
140 - 'is_auth_require_idea_submit' => 'required',
141 - 'is_auth_require_voting_commenting' => 'required',
142 - 'is_auth_require_reaction' => 'required',
143 - 'is_allow_email_along_with_auth' => 'nullable',
144 - 'is_allow_unauthentication_reaction_along_with_auth' => 'nullable'
145 - ]);
146 -
147 - try {
148 - $boards = $this->boardService->modifyAuthenticationPermission($boardData, $board_id);
149 -
150 - return $this->sendSuccess([
151 - 'message' => __("Board has been updated", 'fluent-boards'),
152 - 'boards' => $boards,
153 - ], 200);
154 - } catch (\Exception $e) {
155 - return $this->sendError($e->getMessage(), 404);
156 - }
157 - }
158 -
261 + /*
262 + * TODO: Refactor this method , remove this
263 + */
159 264 public function getBoardsByType($type)
160 265 {
161 266 $boards = $this->boardService->getBoardsByType($type);
162 267
@@ -161,9 +266,9 @@
161 266 $boards = $this->boardService->getBoardsByType($type);
162 267
163 268 return $this->sendSuccess([
164 269 'boards' => $boards,
165 - ], 200);
270 + ]);
166 271 }
167 272
168 273 public function createFirstBoard(Request $request)
169 274 {
@@ -174,11 +279,14 @@
174 279 'currency' => 'nullable|string',
175 280 'crm_contact_id' => 'nullable|numeric',
176 281 ]);
177 282
178 - $installFluentCRM = $request->get('withFluentCRM') == 'yes' ? true : false;
283 + $installFluentCRM = $request->getSafe('withFluentCRM', 'sanitize_text_field') == 'yes' ? true : false;
179 284
180 285 $postStages = $request->get('stages');
286 + if (!is_array($postStages)) {
287 + $postStages = [];
288 + }
181 289 $stageData = array();
182 290 foreach ($postStages as $stage) {
183 291 $temp = $this->stageSanitizeAndValidate($stage, [
184 292 'title' => 'required|string',
@@ -188,9 +296,9 @@
188 296
189 297 $taskData = null;
190 298 if ($request->get('task')) {
191 299 $taskData = $this->taskSanitizeAndValidate($request->get('task'), [
192 - 'title' => 'required|string'
300 + 'title' => 'required|string',
193 301 ]);
194 302 }
195 303
196 304 $board = $this->boardService->createBoard($boardData);
@@ -204,9 +312,9 @@
204 312 $this->taskService->createTask($taskData, $board->id);
205 313 }
206 314
207 315 do_action('fluent_boards/board_created', $board);
208 -
316 +
209 317 if ($installFluentCRM && !defined('FLUENTCRM')) {
210 318 InstallService::install('fluent-crm');
211 319 }
212 320
@@ -215,8 +323,21 @@
215 323 'board' => $board,
216 324 ];
217 325 }
218 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 +
219 340 public function create(Request $request)
220 341 {
221 342 $boardData = $this->boardSanitizeAndValidate($request->get('board'), [
222 343 'title' => 'required|string',
@@ -223,17 +344,46 @@
223 344 'description' => 'nullable',
224 345 'type' => 'required|string',
225 346 'currency' => 'nullable|string',
226 347 'crm_contact_id' => 'nullable|numeric',
348 + 'folder_id' => 'nullable',
227 349 ]);
228 350
229 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 +
230 365 $board = $this->boardService->createBoard($boardData);
231 - $this->labelService->createDefaultLabel($board->id);
366 + $this->createBoardLabelsFromRequest($request, $board->id);
367 + $this->addBoardMembersFromRequest($request, $board->id);
232 368 $type = ucfirst($boardData['type']);
369 + $stages = $request->get('stages');
370 + $sanitizedStages = [];
233 371
234 - if (isset($boardData['is_roadmap']) && $boardData['is_roadmap'] == 'yes') {
235 - $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);
236 386 } else {
237 387 $this->stageService->createDefaultStages($board);
238 388 }
239 389
@@ -243,37 +393,151 @@
243 393 }
244 394
245 395 do_action('fluent_boards/board_created', $board);
246 396
397 +
398 + if ($folderId) {
399 + $folderService->addBoardToFolder($folderId, [$board->id]);
400 + }
401 +
247 402 $message = __('Board has been created successfully', 'fluent-boards');
248 403
249 - return $this->sendSuccess([
404 + return $this->send([
250 405 'message' => $message,
251 406 'board' => $board,
252 407 ], 201);
253 408 } catch (\Exception $e) {
254 - return $this->sendError($e->getMessage(), 400);
409 + return $this->sendError([
410 + 'message' => $e->getMessage()
411 + ]);
255 412 }
256 413 }
257 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 + */
258 528 public function getArchivedStage(Request $request, $board_id)
259 529 {
260 530 try {
261 - $pagination = $request->noPagination ? true : false;
262 - $per_page = isset($data['per_page']) ? $data['per_page'] : 30;
263 - $page = isset($data['page']) ? $data['page'] : 1;
264 - if ($pagination) {
265 - $stages = Stage::where('board_id', $board_id)
266 - ->whereNotNull('archived_at')
267 - ->orderBy('created_at', 'DESC')
268 - ->get();
269 - } else {
270 - $stages = Stage::where('board_id', $board_id)
271 - ->whereNotNull('archived_at')
272 - ->orderBy('created_at', 'DESC')
273 - ->paginate($per_page, ['*'], 'page', $page);
274 - }
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 + ];
275 537
538 + $stages = $this->stageService->getArchivedStages($sanitizedParams, $board_id);
539 +
276 540 return $this->sendSuccess([
277 541 'stages' => $stages,
278 542 ], 200);
279 543 } catch (\Exception $e) {
@@ -280,17 +544,35 @@
280 544 return $this->sendError($e->getMessage(), 404);
281 545 }
282 546 }
283 547
284 - 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)
285 550 {
551 + $syncStartedAt = current_time('mysql');
286 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);
287 555 $board->background = maybe_unserialize($board->background);
288 556 $board->createdOn = $board->created_at->format('Y-m-d');
289 557
290 - $board->load(['users', 'stages', 'labels', 'owner']);
558 + $board->load(['users', 'labels', 'owner']);
291 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 +
292 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 +
293 575 $board->load(['customFields']);
294 576 }
295 577
296 578 $this->boardService->updateRecentBoards($board_id);
@@ -296,21 +578,35 @@
296 578 $this->boardService->updateRecentBoards($board_id);
297 579
298 580 $board->labelColor = Constant::TRELLO_COLOR_MAP;
299 581 $board->labelColorText = Constant::TEXT_COLOR_MAP;
582 + $board->labelColorPresets = Constant::LABEL_COLOR_PRESETS;
300 583
301 584 $board->users = Helper::sanitizeUserCollections($board->users);
302 585 $board->owner = Helper::sanitizeUserCollections($board->owner);
303 586
587 + $board->is_pinned = $this->boardService->isPinned($board->id);
588 +
304 589 $board = apply_filters('fluent_boards/board_find', $board);
305 590
306 591 return [
307 - 'board' => $board
592 + 'board' => $board,
593 + 'synced_at' => $syncStartedAt
308 594 ];
309 595 }
310 596
311 597 public function update(Request $request, $board_id)
312 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 +
313 609 $boardData = $this->boardSanitizeAndValidate($request->only(['title', 'description']), [
314 610 'title' => 'required|string',
315 611 'description' => 'nullable|string',
316 612 ]);
@@ -315,8 +611,9 @@
315 611 'description' => 'nullable|string',
316 612 ]);
317 613
318 614 $board = Board::findOrFail($board_id);
615 + $boardData['description'] = DescriptionMarkdownConverter::normalize($boardData['description']);
319 616
320 617 $oldBoard = clone $board;
321 618 $board->fill($boardData);
322 619 $board->save();
@@ -323,21 +620,23 @@
323 620
324 621 do_action('fluent_boards/board_updated', $board, $oldBoard);
325 622
326 623 return [
327 - 'stages' => $board->stages()->get(),
328 624 'message' => __('Board has been updated', 'fluent-boards'),
329 625 'board' => $board,
626 + 'stages' => $board->stages()->get(),
330 627 ];
331 628 }
332 629
333 630 public function archiveStage($board_id, $stage_id)
334 631 {
632 + $board_id = absint($board_id);
633 + $stage_id = absint($stage_id);
634 +
335 635 try {
336 - $stage = Stage::findOrFail($stage_id);
337 - $board = Board::findOrFail($stage->board_id);
636 + $stage = $this->findStageOnBoard($stage_id, $board_id);
338 637
339 - $updatedStage = $this->boardService->archiveStage($board->id, $stage);
638 + $updatedStage = $this->boardService->archiveStage($board_id, $stage);
340 639
341 640 return $this->sendSuccess([
342 641 'updatedStage' => $updatedStage,
343 642 'message' => __('Stage has been archived', 'fluent-boards'),
@@ -348,18 +647,20 @@
348 647 }
349 648
350 649 public function restoreStage($board_id, $stage_id)
351 650 {
651 + $board_id = absint($board_id);
652 + $stage_id = absint($stage_id);
653 +
352 654 try {
353 - $stage = Stage::findOrFail($stage_id);
354 - $board = Board::findOrFail($board_id);
655 + $stage = $this->findStageOnBoard($stage_id, $board_id);
355 656
356 - $updatedStage = $this->boardService->restoreStage($board->id, $stage);
657 + $updatedStage = $this->boardService->restoreStage($board_id, $stage);
357 658
358 659 return $this->sendSuccess([
359 - 'success' => true,
660 + 'success' => true,
360 661 'updatedStage' => $updatedStage,
361 - 'message' => __('Stage has been restored', 'fluent-boards')
662 + 'message' => __('Stage has been restored', 'fluent-boards')
362 663 ], 200);
363 664 } catch (\Exception $e) {
364 665 return $this->sendError($e->getMessage(), 400);
365 666 }
@@ -365,32 +666,22 @@
365 666 }
366 667 }
367 668
368 669
369 - public function changePositionOfStage(Request $request, $board_id)
670 + public function repositionStages(Request $request, $board_id)
370 671 {
371 - $changeData = $this->boardSanitizeAndValidate($request->only(['fromPosition', 'toPosition']), [
372 - 'fromPosition' => 'required',
373 - 'toPosition' => 'required',
374 - ]);
375 -
672 + $incomingList = $request->get('list');
673 + if (!is_array($incomingList)) {
674 + $incomingList = [];
675 + }
676 + $incomingList = array_map('intval', $incomingList);
376 677 try {
377 - $this->boardService->changePositionOfStage($board_id, $changeData);
678 + foreach ($incomingList as $stageId) {
679 + $this->findStageOnBoard($stageId, $board_id);
680 + }
378 681
682 + $this->boardService->repositionStages($board_id, $incomingList);
379 683 return $this->sendSuccess([
380 - 'message' => __('Board stage has been updated', 'fluent-boards')
381 - ], 200);
382 - } catch (\Exception $e) {
383 - return $this->sendError($e->getMessage(), 400);
384 - }
385 - }
386 -
387 - public function rePositionStages(Request $request, $board_id)
388 - {
389 - $incomingList = $request->get('list');
390 - try {
391 - $this->boardService->rePositionStages($board_id, $incomingList);
392 - return $this->sendSuccess([
393 684 'message' => __('Stages Reordered', 'fluent-boards'),
394 685 'updatedStages' => $this->stageService->getLastOneMinuteUpdatedStages($board_id)
395 686 ], 200);
396 687 } catch (\Exception $e) {
@@ -408,9 +699,9 @@
408 699 public function delete($board_id)
409 700 {
410 701 try {
411 702 if (!PermissionManager::isAdmin()) {
412 - 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);
413 704 }
414 705 $this->boardService->deleteBoard($board_id);
415 706
416 707 return $this->sendSuccess([
@@ -428,9 +719,12 @@
428 719
429 720 public function getActivities(Request $request, $board_id)
430 721 {
431 722 try {
432 - $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 + ]);
433 727 return $this->sendSuccess([
434 728 'activities' => $activities,
435 729 ], 200);
436 730 } catch (\Exception $e) {
@@ -437,8 +731,11 @@
437 731 return $this->sendError($e->getMessage(), 404);
438 732 }
439 733 }
440 734
735 + /*
736 + * TODO: Refactor this method - for Masiur
737 + */
441 738 public function getBoardUsers($board_id)
442 739 {
443 740 $board = Board::findOrFail($board_id);
444 741
@@ -445,8 +742,11 @@
445 742 $boardObjects = Relation::where('object_type', 'board_user')
446 743 ->where('object_id', $board_id)
447 744 ->get()->keyBy('foreign_id');
448 745
746 + $superAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
747 + ->get()->pluck('object_id')->toArray();
748 +
449 749 $userIds = $boardObjects->pluck('foreign_id')->toArray();
450 750
451 751 $coreUsers = [];
452 752 if ($userIds) {
@@ -465,14 +765,17 @@
465 765 }
466 766
467 767 $boardRelation = $boardObjects[$user->ID] ?? null;
468 768
769 +
469 770 $formattedUsers[] = [
470 771 'ID' => $user->ID,
471 772 'display_name' => $name,
472 773 'email' => $user->user_email,
473 774 'photo' => fluent_boards_user_avatar($user->user_email, $name),
474 - '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')
475 778 ];
476 779 }
477 780
478 781 // order formatted users by display_name
@@ -480,9 +783,9 @@
480 783 return strcmp($a['display_name'], $b['display_name']);
481 784 });
482 785
483 786 $returnData = [
484 - 'users' => Helper::sanitizeUsersArray($formattedUsers),
787 + 'users' => Helper::sanitizeUsersArray($formattedUsers, $board_id),
485 788 'global_admins' => []
486 789 ];
487 790
488 791 if (!PermissionManager::isAdmin(get_current_user_id())) {
@@ -489,16 +792,24 @@
489 792 return $returnData;
490 793 }
491 794
492 795 /*
493 - * 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.
494 797 */
495 - $adminUserIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
798 + $fluentBoardAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
496 799 ->whereNotIn('object_id', $userIds)
497 800 ->get()
498 801 ->pluck('object_id')
499 802 ->toArray();
500 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 +
501 812 if ($adminUserIds) {
502 813 $adminUsers = get_users([
503 814 'include' => $adminUserIds,
504 815 ]);
@@ -515,9 +826,11 @@
515 826 'ID' => $user->ID,
516 827 'display_name' => $name,
517 828 'email' => $user->user_email,
518 829 'photo' => fluent_boards_user_avatar($user->user_email, $name),
519 - 'role' => 'admin'
830 + 'role' => 'admin',
831 + 'is_super' => in_array($user->ID, $superAdminIds),
832 + 'is_wpadmin' => $user->has_cap('manage_options')
520 833 ];
521 834 }
522 835
523 836 // order formatted users by display_name
@@ -524,9 +837,9 @@
524 837 usort($formattedAdminUsers, function ($a, $b) {
525 838 return strcmp($a['display_name'], $b['display_name']);
526 839 });
527 840
528 - $returnData['global_admins'] = Helper::sanitizeUsersArray($formattedAdminUsers);
841 + $returnData['global_admins'] = Helper::sanitizeUsersArray($formattedAdminUsers, $board_id);
529 842 }
530 843
531 844 return $this->sendSuccess($returnData, 200);
532 845 }
@@ -546,20 +859,28 @@
546 859 }
547 860
548 861 public function addMembersInBoard(Request $request, $board_id)
549 862 {
550 - $memberId = $request->getSafe('memberId');
551 - $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);
552 866
553 - if ($isAlreadyMember) {
867 + if ($member === null) {
554 868 return $this->sendError([
869 + 'message' => __('User not found.', 'fluent-boards'),
870 + ], 404);
871 + }
872 +
873 + if (!$member) {
874 + return $this->sendError([
555 875 'message' => __('User already a member', 'fluent-boards'),
556 - ], 304);
876 + ], 409);
557 877 }
558 - $member = $this->boardService->addMembersInBoard($board_id, $memberId);
559 878
879 +
560 880 return [
561 881 'message' => __('Member added successfully', 'fluent-boards'),
882 + 'is_admin' => PermissionManager::isAdmin($memberId),
562 883 'member' => Helper::sanitizeUserCollections($member)
563 884 ];
564 885 }
565 886
@@ -585,11 +906,11 @@
585 906 }
586 907
587 908 public function searchBoards(Request $request)
588 909 {
589 - $per_page = $request->get('per_page', 10);
590 - $search_input = $request->searchInput . trim('');
591 - $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');
592 913
593 914 $currentUserId = get_current_user_id();
594 915
595 916 if (PermissionManager::isAdmin($currentUserId)) {
@@ -631,10 +952,13 @@
631 952 * @return
632 953 */
633 954 public function changeStageView($board_id, $stage_id)
634 955 {
956 + $board_id = absint($board_id);
957 + $stage_id = absint($stage_id);
958 +
635 959 try {
636 - $stage = Stage::findOrFail($stage_id);
960 + $stage = $this->findStageOnBoard($stage_id, $board_id);
637 961 $message = __('The stage is made public!', 'fluent-boards');
638 962 $settings = $stage->settings;
639 963
640 964 if (isset($settings['is_public'])) {
@@ -639,9 +963,9 @@
639 963
640 964 if (isset($settings['is_public'])) {
641 965 if ($settings['is_public']) {
642 966 $settings['is_public'] = false;
643 - $message = __('The stage is made admin only!', 'fluent-boards');
967 + $message = __('The stage is made private!', 'fluent-boards');
644 968 } else {
645 969 $settings['is_public'] = true;
646 970 }
647 971 } else {
@@ -660,24 +984,27 @@
660 984 }
661 985
662 986
663 987 /**
664 - * Set board background image or color
988 + * Set or reset board background image/color.
665 989 * @param \FluentBoards\Framework\Http\Request\Request $request
666 990 * @return
667 991 */
668 992 public function setBoardBackground(Request $request, $board_id)
669 993 {
670 - // sanitize and validate image_url
671 - 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) {
672 1002 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
673 - "id" => 'required',
1003 + 'id' => 'required|integer',
674 1004 'image_url' => 'required|string|url',
675 1005 ]);
676 - }
677 -
678 - // sanitize and validate color
679 - if ($request->color) {
1006 + } elseif ($request->color) {
680 1007 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
681 1008 "id" => 'required',
682 1009 'color' => 'required',
683 1010 ]);
@@ -685,17 +1012,22 @@
685 1012
686 1013 try {
687 1014 if (!$board_id) {
688 1015 $errorMessage = __('Board id is required', 'fluent-boards');
689 - throw new \Exception($errorMessage, 400);
1016 + throw new \Exception(esc_html($errorMessage), 400);
690 1017 }
691 1018
1019 + if (empty($backgroundData)) {
1020 + $errorMessage = __('Background data is required', 'fluent-boards');
1021 + throw new \Exception(esc_html($errorMessage), 400);
1022 + }
1023 +
692 1024 return $this->sendSuccess([
693 1025 'message' => __('Background updated successfully', 'fluent-boards'),
694 1026 'background' => $this->boardService->setBoardBackground($backgroundData, $board_id),
695 1027 ]);
696 1028 } catch (\Exception $e) {
697 - $this->sendError([$e->getMessage(), 400]);
1029 + return $this->sendError($e->getMessage(), 400);
698 1030 }
699 1031 }
700 1032
701 1033
@@ -705,15 +1037,19 @@
705 1037 * @param mixed $stage_slug
706 1038 * @return $availablePositions as an array
707 1039 * @throws \Exception
708 1040 */
709 - public function getStageTaskAvailablePositions($board_id, $stage_id)
1041 + public function getStageTaskAvailablePositions(Request $request, $board_id, $stage_id)
710 1042 {
711 1043 try {
712 1044 if ($board_id && $stage_id) {
713 - $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);
714 1047 return $this->sendSuccess([
715 - 'availablePositions' => $availablePositions
1048 + 'availablePositions' => $availablePositions['availablePositions'],
1049 + 'moveTargets' => $availablePositions['moveTargets'],
1050 + 'currentMoveTargetKey' => $availablePositions['currentMoveTargetKey'],
1051 + 'defaultMoveTargetKey' => $availablePositions['defaultMoveTargetKey'],
716 1052 ], 200);
717 1053 } else {
718 1054 $message = '';
719 1055 if (!$board_id) {
@@ -721,12 +1057,12 @@
721 1057 }
722 1058 if (!$stage_id) {
723 1059 $message = 'Stage ';
724 1060 }
725 - throw new \Exception($message . 'is required', 400);
1061 + throw new \Exception(esc_html($message . 'is required'), 400);
726 1062 }
727 1063 } catch (\Exception $e) {
728 - $this->sendError([$e->getMessage(), 400]);
1064 + return $this->sendError($e->getMessage(), 400);
729 1065 }
730 1066 }
731 1067
732 1068 public function getAssociateCrmContacts($board_id)
@@ -735,24 +1071,47 @@
735 1071 $contactAssociatedTasks = Task::with('board')->where('board_id', $board_id)
736 1072 ->whereNotNull('crm_contact_id')
737 1073 ->get();
738 1074
739 - $formattedContacts = Collection::make($contactAssociatedTasks)
740 - ->groupBy('crm_contact_id')
741 - ->map(function ($tasks, $contactId) {
742 - $subscriber = Subscriber::find($contactId);
743 - 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) {
744 1104 return null; // Skip if subscriber not found
745 1105 }
746 1106
747 - return [
748 - 'name' => $subscriber->first_name . ' ' . $subscriber->last_name,
749 - 'photo' => $subscriber->photo,
750 - 'email' => $subscriber->email,
751 - 'crm_contact_id' => $contactId,
752 - 'id' => $contactId,
753 - 'tasks' => $tasks,
754 - ];
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;
755 1114 })
756 1115 ->filter()->toArray();
757 1116
758 1117
@@ -773,11 +1132,13 @@
773 1132 'message' => __('Associated Crm Member has been updated', 'fluent-boards'),
774 1133 ], 200);
775 1134 }
776 1135
777 - public function hasDataChanged($board_id)
1136 + public function hasDataChanged(Request $request, $board_id)
778 1137 {
779 - 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);
780 1141 }
781 1142
782 1143 public function createStage(Request $request, $board_id)
783 1144 {
@@ -782,8 +1143,9 @@
782 1143 public function createStage(Request $request, $board_id)
783 1144 {
784 1145 $stageData = $this->stageSanitizeAndValidate($request->all(), [
785 1146 'title' => 'required|string',
1147 + 'position' => 'nullable|numeric'
786 1148 ]);
787 1149
788 1150 $board = Board::find($board_id);
789 1151 $stage = $this->stageService->createStage($stageData, $board_id);
@@ -799,15 +1161,27 @@
799 1161 }
800 1162
801 1163 public function moveAllTasks(Request $request, $board_id)
802 1164 {
803 - $oldStageId = $request->getSafe('oldStageId');
804 - $newStageId = $request->getSafe('newStageId');
1165 + $oldStageId = $request->getSafe('oldStageId', 'intval');
1166 + $newStageId = $request->getSafe('newStageId', 'intval');
805 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 +
806 1180 $updates = $this->stageService->moveAllTasks($oldStageId, $newStageId, $board_id);
807 1181
808 1182 return [
809 - 'message' => __('Tasks has been Moved', 'fluent-boards'),
1183 + 'message' => __('Tasks have been moved', 'fluent-boards'),
810 1184 'updatedTasks' => $updates,
811 1185 ];
812 1186
813 1187 }
@@ -813,52 +1187,103 @@
813 1187 }
814 1188
815 1189 public function archiveAllTasksInStage($board_id, $stage_id)
816 1190 {
817 - $updates = $this->stageService->archiveAllTasksInStage($stage_id);
818 - return [
819 - 'message' => __('Tasks has been archived', 'fluent-boards'),
820 - 'updatedTasks' => $updates,
821 - ];
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 + }
822 1205 }
823 1206
824 1207 public function getAssociatedBoards(Request $request, $associated_id)
825 1208 {
826 - $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 +
827 1221 return [
828 1222 'boards' => $associatedBoards,
829 1223 ];
830 1224 }
831 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 + */
832 1240 public function duplicateBoard(Request $request, $board_id)
833 1241 {
834 1242 $boardData = $this->taskSanitizeAndValidate($request->get('board'), [
835 1243 'title' => 'required|string'
836 1244 ]);
1245 +
1246 + $boardData['source_board_id'] = $board_id;
1247 +
837 1248 $isWithLabels = $request->getSafe('isWithLabels');
838 1249 $isWithTasks = $request->getSafe('isWithTasks');
1250 + $isWithMembers = $request->getSafe('isWithMembers');
1251 + $isWithCustomFields = $request->getSafe('isWithCustomFields', 'sanitize_text_field', 'yes');
1252 + $isWithTemplates = $request->getSafe('isWithTemplates');
839 1253
840 1254 try {
841 1255 if(!PermissionManager::isAdmin()) {
842 1256 $errorMessage = __('You do not have permission to duplicate board', 'fluent-boards');
843 - throw new \Exception($errorMessage, 400);
1257 + throw new \Exception(esc_html($errorMessage), 400);
844 1258 }
845 1259 //create board
846 1260 $newBoard = $this->boardService->copyBoard($boardData);
1261 + $customFieldMap = [];
847 1262
1263 + if ($isWithCustomFields == 'yes') {
1264 + $customFieldMap = $this->boardService->copyCustomFieldsOfBoard($board_id, $newBoard);
1265 + }
1266 +
848 1267 //label copy
1268 + $labelMap = [];
1269 +
849 1270 if ($isWithLabels == 'yes') {
850 - $this->labelService->copyLabelsOfBoard($board_id, $newBoard);
1271 + $labelMap = $this->labelService->copyLabelsOfBoard($board_id, $newBoard);
851 1272 }
852 1273
853 1274 //stage copy
854 - $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id);
1275 + $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id, $isWithTemplates);
855 1276
856 1277 //copy tasks of selected stages
857 1278 if ($isWithTasks == 'yes') {
858 - $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard);
1279 + $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap, $isWithTemplates, $customFieldMap);
859 1280 }
860 1281
1282 + if ($isWithMembers == 'yes') {
1283 + $this->boardService->copyMembersOfBoard($board_id, $newBoard);
1284 + }
1285 +
861 1286 return $this->sendSuccess([
862 1287 'board' => $newBoard,
863 1288 ], 200);
864 1289 } catch (\Exception $e) {
@@ -868,11 +1293,18 @@
868 1293
869 1294 public function importFromBoard(Request $request, $board_id)
870 1295 {
871 1296 $selectedStages = $request->getSafe('selectedStages');
1297 + $position = $request->getSafe('position', 'intval');
872 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 +
873 1305 try {
874 - $this->stageService->importStagesFromBoard($board_id, $selectedStages);
1306 + $this->stageService->importStagesFromBoard($board_id, $selectedStages, $position);
875 1307
876 1308 return $this->sendSuccess([
877 1309 'message' => __('Import successfully', 'fluent-boards'),
878 1310 ], 200);
@@ -887,7 +1319,242 @@
887 1319 return [
888 1320 'solidColors' => Constant::BOARD_BACKGROUND_DEFAULT_SOLID_COLORS,
889 1321 'gradients' => Constant::BOARD_BACKGROUND_DEFAULT_GRADIENT_COLORS
890 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;
891 1558 }
892 1559
893 1560 }