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