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