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 +784 -185 1.12trunk View file →
@@ -1,14 +1,17 @@
1 1 <?php
2 2
3 3 namespace FluentBoards\App\Http\Controllers;
4 4
5 +use FluentBoards\App\Models\Attachment;
5 6 use FluentBoards\App\Models\Meta;
6 7 use FluentBoards\App\Models\Relation;
7 8 use FluentBoards\App\Models\Task;
8 9 use FluentBoards\App\Models\User;
9 10 use FluentBoards\App\Models\Board;
11 +use FluentBoards\App\Services\CommentService;
10 12 use FluentBoards\App\Services\Constant;
13 +use FluentBoards\App\Services\DescriptionMarkdownConverter;
11 14 use FluentBoards\App\Services\Helper;
12 15 use FluentBoards\App\Models\Stage;
13 16 use FluentBoards\App\Services\InstallService;
14 17 use FluentBoards\App\Services\StageService;
@@ -13,19 +16,26 @@
13 16 use FluentBoards\App\Services\InstallService;
14 17 use FluentBoards\App\Services\StageService;
15 18 use FluentBoards\App\Services\TaskService;
16 19 use FluentBoards\App\Services\BoardService;
17 -use FluentBoards\App\Services\UserService;
20 +use FluentBoards\App\Services\FolderService;
21 +use FluentBoards\App\Services\UploadService;
18 22 use FluentBoards\Framework\Http\Request\Request;
19 23 use FluentBoards\App\Services\PermissionManager;
24 +use FluentBoards\App\Services\PublicAccessService;
20 25 use FluentBoards\App\Hooks\Handlers\BoardHandler;
26 +use FluentBoards\App\Hooks\Handlers\BoardMenuHandler;
21 27 use FluentBoards\App\Services\LabelService;
22 28 use FluentBoards\Framework\Support\Arr;
23 29 use FluentBoards\Framework\Support\Collection;
24 -use FluentCrm\App\Models\Subscriber;
30 +use FluentBoardsPro\App\Services\AttachmentService;
31 +use FluentBoardsPro\App\Services\CustomFieldService;
32 +use FluentBoardsPro\App\Services\RemoteUrlParser;
25 33
26 34 class BoardController extends Controller
27 35 {
36 + private const LEGACY_BOARD_ASSOCIATED_CRM_CONTACT = 'crm_contact';
37 +
28 38 private $boardService;
29 39 private $taskService;
30 40 private $stageService;
31 41 private $labelService;
@@ -45,9 +55,10 @@
45 55 }
46 56
47 57 public function getBoards(Request $request)
48 58 {
49 - $per_page = $request->getSafe('per_page', 'intval', 20);
59 + $per_page = $request->getSafe('per_page', 'intval', 100);
60 + $per_page = max(1, min(100, $per_page));
50 61 $userId = get_current_user_id();
51 62 $type = $request->getSafe('type', 'sanitize_text_field', 'to-do');
52 63
53 64 $order = $request->getSafe('order', 'sanitize_text_field', 'created_at');
@@ -53,10 +64,48 @@
53 64 $order = $request->getSafe('order', 'sanitize_text_field', 'created_at');
54 65 $orderBy = $request->getSafe('orderBy', 'sanitize_text_field', 'DESC');
55 66 $searchInput = $request->getSafe('searchInput', 'sanitize_text_field');
56 67
57 - $relatedBoardsQuery = Board::where('type', $type)
58 - ->byAccessUser($userId);
68 + $option = $request->getSafe('option', 'sanitize_text_field');
69 + $folderId = $request->getSafe('fid', 'intval'); // Get folder ID from request
70 +
71 + // Initialize the query based on archive status
72 + if (!defined('FLUENT_ROADMAP')) {
73 + if ($option == 'archived') {
74 + $relatedBoardsQuery = Board::whereNotNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
75 + } else {
76 + $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
77 + }
78 + } else {
79 + if ($option == 'archived') {
80 + $relatedBoardsQuery = Board::whereNotNull('archived_at')->byAccessUser($userId);
81 + } else {
82 + $relatedBoardsQuery = Board::whereNull('archived_at')->byAccessUser($userId);
83 + }
84 + }
85 +
86 + // Scope pinned before pagination, from the same id source as getBoardCounts(),
87 + // so the pinned page and board_counts.pinned always agree. Filtering pinned
88 + // after pagination would drop pinned boards that fall on a later active page.
89 + if ($option == 'pinned') {
90 + $pinnedIds = $this->boardService->getPinnedBoardIds();
91 +
92 + $relatedBoardsQuery = $pinnedIds
93 + ? $relatedBoardsQuery->whereIn('id', $pinnedIds)
94 + : $relatedBoardsQuery->where('id', 0);
95 + }
96 +
97 + if ($folderId) {
98 + $boardIds = (new FolderService())->getBoardIdsByFolder($folderId);
99 + $relatedBoardsQuery = $boardIds
100 + ? $relatedBoardsQuery->whereIn('id', $boardIds)
101 + : $relatedBoardsQuery->where('id', 0);
102 + }
103 +
104 + // Filter out boards that are templates (exclude boards where settings->is_template is true)
105 + $relatedBoardsQuery = $relatedBoardsQuery->excludeTemplates();
106 +
107 + // Add search functionality
59 108 if (!empty($searchInput)) {
60 109 $relatedBoardsQuery = $relatedBoardsQuery->where('title', 'like', '%' . $searchInput . '%');
61 110 }
62 111
@@ -65,38 +114,114 @@
65 114 ->with('stages', 'users')
66 115 ->paginate($per_page);
67 116
68 117 foreach ($relatedBoards as $relatedBoard) {
118 + $relatedBoard->description = DescriptionMarkdownConverter::normalize($relatedBoard->description);
69 119 $relatedBoard->users = Helper::sanitizeUserCollections($relatedBoard->users);
120 + $relatedBoard->is_pinned = $this->boardService->isPinned($relatedBoard->id);
70 121 }
71 122
123 + $response = [
124 + 'boards' => $relatedBoards,
125 + 'board_counts' => $this->boardService->getBoardCounts($userId)
126 + ];
127 +
128 + $folderMapping = $this->getBoardFolderMapping($userId);
129 + $response['folder_mapping'] = $folderMapping;
130 + if ($folderId) {
131 + $response['current_folder'] = isset($folderMapping[$folderId])
132 + ? $this->getCurrentFolderInfoFromMapping($folderMapping[$folderId])
133 + : null;
134 + }
135 +
136 + return $this->sendSuccess($response);
137 + }
138 +
139 + /**
140 + * Get folder mapping for boards
141 + */
142 + private function getBoardFolderMapping($userId)
143 + {
144 + $folderService = new FolderService();
145 + $folders = $folderService->getFolders($userId);
146 +
147 + $mapping = [];
148 + foreach ($folders as $folder) {
149 + $mapping[$folder->id] = [
150 + 'id' => $folder->id,
151 + 'title' => $folder->title,
152 + 'board_ids' => $folder->boards ? $folder->boards->pluck('id')->toArray() : []
153 + ];
154 + }
155 +
156 + return $mapping;
157 + }
158 +
159 + private function getCurrentFolderInfoFromMapping(array $folder)
160 + {
161 + return [
162 + 'id' => $folder['id'],
163 + 'title' => $folder['title'],
164 + 'board_count' => count($folder['board_ids'])
165 + ];
166 + }
167 +
168 + /**
169 + * Get the list of boards and their associated stages for the current user.
170 + *
171 + * @param \FluentBoards\Framework\Http\Request\Request $request
172 + * @return \WP_REST_Response
173 + */
174 + public function getBoardsList(Request $request)
175 + {
176 + $userId = get_current_user_id();
177 +
178 + // Query to fetch boards that are not archived and accessible by the user
179 + // Check if the FLUENT_ROADMAP constant is defined
180 + if (!defined('FLUENT_ROADMAP')) {
181 + $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->excludeTemplates()->byAccessUser($userId);
182 + } else {
183 + $relatedBoardsQuery = Board::whereNull('archived_at')->excludeTemplates()->byAccessUser($userId);
184 + }
185 +
186 + $relatedBoards = $relatedBoardsQuery->with('stages')->get();
187 +
188 + // Fetch the stages associated with the boards
189 + $stages = Stage::whereIn('board_id', $relatedBoards->pluck('id'))->where('archived_at', null)->get();
190 +
72 191 return $this->sendSuccess([
73 - 'boards' => $relatedBoards
192 + 'boards' => $relatedBoards,
193 + 'all_stages' => $stages,
74 194 ], 200);
75 195 }
76 -
77 - public function getBoardsList(Request $request)
196 + public function getOnlyBoardsByUser(Request $request)
78 197 {
79 198 try {
80 199 $userId = get_current_user_id();
81 - $type = $request->getSafe('type', 'sanitize_text_field', 'to-do');
82 200
83 - if (PermissionManager::isAdmin($userId)) {
84 - $relatedBoardsQuery = Board::query()->where('type', $type);
201 + $searchInput = $request->getSafe('searchInput', 'sanitize_text_field');
202 +
203 +
204 + if(!defined('FLUENT_ROADMAP'))
205 + {
206 + $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->excludeTemplates()->byAccessUser($userId);
85 207 } else {
86 - $currentUser = User::find($userId);
87 - $relatedBoardsQuery = $currentUser->whichBoards()->where('type', $type);
208 + $relatedBoardsQuery = Board::whereNull('archived_at')->excludeTemplates()->byAccessUser($userId);
88 209 }
89 210
90 - $relatedBoards = $relatedBoardsQuery->with('stages')->get();
91 - $stages = Stage::whereIn('board_id', $relatedBoards->pluck('id'))->get();
211 + if (!empty($searchInput)) {
212 + $relatedBoardsQuery = $relatedBoardsQuery->where('title', 'like', '%' . $searchInput . '%');
213 + }
92 214
215 + $relatedBoards = $relatedBoardsQuery->orderBy('created_at', 'DESC')->get();
216 +
93 217 return $this->sendSuccess([
94 - 'boards' => $relatedBoards,
95 - 'all_stages' => $stages,
96 - ], 200);
218 + 'boards' => $relatedBoards
219 + ]);
97 220 } catch (\Exception $e) {
98 - return $this->sendError($e->getMessage(), 404);
221 + return $this->sendError([
222 + 'message' => $e->getMessage()
223 + ]);
99 224 }
100 225 }
101 226
102 227 public function getRecentBoards()
@@ -103,9 +228,12 @@
103 228 {
104 229 $boards = $this->boardService->getRecentBoards();
105 230
106 231 if (!$boards || $boards->isEmpty()) {
107 - $boards = Board::where('type', 'to-do')->byAccessUser(get_current_user_id())
232 + $boards = Board::whereNull('archived_at')
233 + ->excludeTemplates()
234 + ->availableInCurrentInstall()
235 + ->byAccessUser(get_current_user_id())
108 236 ->limit(4)
109 237 ->withCount('completedTasks')
110 238 ->with(['stages', 'users'])
111 239 ->get();
@@ -112,8 +240,9 @@
112 240 }
113 241
114 242 foreach ($boards as $board) {
115 243 $board->users = Helper::sanitizeUserCollections($board->users);
244 + $board->is_pinned = $this->boardService->isPinned($board->id);
116 245 }
117 246
118 247 return [
119 248 'boards' => $boards,
@@ -119,45 +248,11 @@
119 248 'boards' => $boards,
120 249 ];
121 250 }
122 251
123 - public function getBoardMeta($board_id)
124 - {
125 - $boards = $this->boardService->fetchBoardMeta($board_id);
126 -
127 - return $this->sendSuccess([
128 - 'boards' => $boards,
129 - ], 200);
130 - }
131 -
132 - public function setAuthenticationPermission(Request $request, $board_id)
133 - {
134 - $boardData = $this->boardSanitizeAndValidate($request->only([
135 - 'is_auth_require_idea_submit',
136 - 'is_auth_require_voting_commenting',
137 - 'is_auth_require_reaction',
138 - 'is_allow_email_along_with_auth',
139 - 'is_allow_unauthentication_reaction_along_with_auth'
140 - ]), [
141 - 'is_auth_require_idea_submit' => 'required',
142 - 'is_auth_require_voting_commenting' => 'required',
143 - 'is_auth_require_reaction' => 'required',
144 - 'is_allow_email_along_with_auth' => 'nullable',
145 - 'is_allow_unauthentication_reaction_along_with_auth' => 'nullable'
146 - ]);
147 -
148 - try {
149 - $boards = $this->boardService->modifyAuthenticationPermission($boardData, $board_id);
150 -
151 - return $this->sendSuccess([
152 - 'message' => __("Board has been updated", 'fluent-boards'),
153 - 'boards' => $boards,
154 - ], 200);
155 - } catch (\Exception $e) {
156 - return $this->sendError($e->getMessage(), 404);
157 - }
158 - }
159 -
252 + /*
253 + * TODO: Refactor this method , remove this
254 + */
160 255 public function getBoardsByType($type)
161 256 {
162 257 $boards = $this->boardService->getBoardsByType($type);
163 258
@@ -162,9 +257,9 @@
162 257 $boards = $this->boardService->getBoardsByType($type);
163 258
164 259 return $this->sendSuccess([
165 260 'boards' => $boards,
166 - ], 200);
261 + ]);
167 262 }
168 263
169 264 public function createFirstBoard(Request $request)
170 265 {
@@ -175,11 +270,14 @@
175 270 'currency' => 'nullable|string',
176 271 'crm_contact_id' => 'nullable|numeric',
177 272 ]);
178 273
179 - $installFluentCRM = $request->get('withFluentCRM') == 'yes' ? true : false;
274 + $installFluentCRM = $request->getSafe('withFluentCRM', 'sanitize_text_field') == 'yes' ? true : false;
180 275
181 276 $postStages = $request->get('stages');
277 + if (!is_array($postStages)) {
278 + $postStages = [];
279 + }
182 280 $stageData = array();
183 281 foreach ($postStages as $stage) {
184 282 $temp = $this->stageSanitizeAndValidate($stage, [
185 283 'title' => 'required|string',
@@ -189,9 +287,9 @@
189 287
190 288 $taskData = null;
191 289 if ($request->get('task')) {
192 290 $taskData = $this->taskSanitizeAndValidate($request->get('task'), [
193 - 'title' => 'required|string'
291 + 'title' => 'required|string',
194 292 ]);
195 293 }
196 294
197 295 $board = $this->boardService->createBoard($boardData);
@@ -205,9 +303,9 @@
205 303 $this->taskService->createTask($taskData, $board->id);
206 304 }
207 305
208 306 do_action('fluent_boards/board_created', $board);
209 -
307 +
210 308 if ($installFluentCRM && !defined('FLUENTCRM')) {
211 309 InstallService::install('fluent-crm');
212 310 }
213 311
@@ -216,8 +314,21 @@
216 314 'board' => $board,
217 315 ];
218 316 }
219 317
318 + public function skipOnboarding(Request $request)
319 + {
320 + $onboarding = Meta::where('key', Constant::FBS_ONBOARDING)->first();
321 + if($onboarding && $onboarding->value == 'no'){
322 + $onboarding->value = 'yes' ;
323 + $onboarding->save();
324 + }
325 +
326 + return [
327 + 'message' => __('Onboarding skipped successfully', 'fluent-boards'),
328 + ];
329 + }
330 +
220 331 public function create(Request $request)
221 332 {
222 333 $boardData = $this->boardSanitizeAndValidate($request->get('board'), [
223 334 'title' => 'required|string',
@@ -224,17 +335,44 @@
224 335 'description' => 'nullable',
225 336 'type' => 'required|string',
226 337 'currency' => 'nullable|string',
227 338 'crm_contact_id' => 'nullable|numeric',
339 + 'folder_id' => 'nullable',
228 340 ]);
229 341
230 342 try {
343 + $folderId = $request->getSafe('folder_id', 'intval');
344 + $folderService = new FolderService();
345 + if ($folderId) {
346 + $folderService->assertCanModifyFolder($folderId);
347 + }
348 +
349 + $backgroundData = $this->sanitizeCreateBoardBackground($request->get('background'));
350 + if (!empty($backgroundData)) {
351 + $boardData['background'] = $backgroundData;
352 + }
353 +
231 354 $board = $this->boardService->createBoard($boardData);
232 - $this->labelService->createDefaultLabel($board->id);
355 + $this->createBoardLabelsFromRequest($request, $board->id);
356 + $this->addBoardMembersFromRequest($request, $board->id);
233 357 $type = ucfirst($boardData['type']);
358 + $stages = $request->get('stages');
359 + $sanitizedStages = [];
234 360
235 - if (isset($boardData['is_roadmap']) && $boardData['is_roadmap'] == 'yes') {
236 - $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);
237 375 } else {
238 376 $this->stageService->createDefaultStages($board);
239 377 }
240 378
@@ -244,37 +382,118 @@
244 382 }
245 383
246 384 do_action('fluent_boards/board_created', $board);
247 385
386 +
387 + if ($folderId) {
388 + $folderService->addBoardToFolder($folderId, [$board->id]);
389 + }
390 +
248 391 $message = __('Board has been created successfully', 'fluent-boards');
249 392
250 - return $this->sendSuccess([
393 + return $this->send([
251 394 'message' => $message,
252 395 'board' => $board,
253 396 ], 201);
254 397 } catch (\Exception $e) {
255 - return $this->sendError($e->getMessage(), 400);
398 + return $this->sendError([
399 + 'message' => $e->getMessage()
400 + ]);
256 401 }
257 402 }
258 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 + */
259 484 public function getArchivedStage(Request $request, $board_id)
260 485 {
261 486 try {
262 - $pagination = $request->noPagination ? true : false;
263 - $per_page = isset($data['per_page']) ? $data['per_page'] : 30;
264 - $page = isset($data['page']) ? $data['page'] : 1;
265 - if ($pagination) {
266 - $stages = Stage::where('board_id', $board_id)
267 - ->whereNotNull('archived_at')
268 - ->orderBy('created_at', 'DESC')
269 - ->get();
270 - } else {
271 - $stages = Stage::where('board_id', $board_id)
272 - ->whereNotNull('archived_at')
273 - ->orderBy('created_at', 'DESC')
274 - ->paginate($per_page, ['*'], 'page', $page);
275 - }
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 + ];
276 493
494 + $stages = $this->stageService->getArchivedStages($sanitizedParams, $board_id);
495 +
277 496 return $this->sendSuccess([
278 497 'stages' => $stages,
279 498 ], 200);
280 499 } catch (\Exception $e) {
@@ -281,17 +500,38 @@
281 500 return $this->sendError($e->getMessage(), 404);
282 501 }
283 502 }
284 503
285 - public function find($board_id)
504 + public function find(Request $request, $board_id)
286 505 {
287 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);
288 509 $board->background = maybe_unserialize($board->background);
289 510 $board->createdOn = $board->created_at->format('Y-m-d');
290 511
291 - $board->load(['users', 'stages', 'labels', 'owner']);
292 - $this->boardService->updateRecentOpenedBoards($board_id);
512 + $board->load(['users', 'labels', 'owner']);
293 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 +
294 534 $board->labelColor = Constant::TRELLO_COLOR_MAP;
295 535 $board->labelColorText = Constant::TEXT_COLOR_MAP;
296 536
297 537 $board->users = Helper::sanitizeUserCollections($board->users);
@@ -296,17 +536,30 @@
296 536
297 537 $board->users = Helper::sanitizeUserCollections($board->users);
298 538 $board->owner = Helper::sanitizeUserCollections($board->owner);
299 539
540 + $board->is_pinned = $this->boardService->isPinned($board->id);
541 +
300 542 $board = apply_filters('fluent_boards/board_find', $board);
301 543
302 544 return [
303 - 'board' => $board
545 + 'board' => $board,
546 + 'synced_at' => current_time('mysql')
304 547 ];
305 548 }
306 549
307 550 public function update(Request $request, $board_id)
308 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 +
309 562 $boardData = $this->boardSanitizeAndValidate($request->only(['title', 'description']), [
310 563 'title' => 'required|string',
311 564 'description' => 'nullable|string',
312 565 ]);
@@ -311,8 +564,9 @@
311 564 'description' => 'nullable|string',
312 565 ]);
313 566
314 567 $board = Board::findOrFail($board_id);
568 + $boardData['description'] = DescriptionMarkdownConverter::normalize($boardData['description']);
315 569
316 570 $oldBoard = clone $board;
317 571 $board->fill($boardData);
318 572 $board->save();
@@ -319,21 +573,23 @@
319 573
320 574 do_action('fluent_boards/board_updated', $board, $oldBoard);
321 575
322 576 return [
323 - 'stages' => $board->stages()->get(),
324 577 'message' => __('Board has been updated', 'fluent-boards'),
325 578 'board' => $board,
579 + 'stages' => $board->stages()->get(),
326 580 ];
327 581 }
328 582
329 583 public function archiveStage($board_id, $stage_id)
330 584 {
585 + $board_id = absint($board_id);
586 + $stage_id = absint($stage_id);
587 +
331 588 try {
332 - $stage = Stage::findOrFail($stage_id);
333 - $board = Board::findOrFail($stage->board_id);
589 + $stage = $this->findStageOnBoard($stage_id, $board_id);
334 590
335 - $updatedStage = $this->boardService->archiveStage($board->id, $stage);
591 + $updatedStage = $this->boardService->archiveStage($board_id, $stage);
336 592
337 593 return $this->sendSuccess([
338 594 'updatedStage' => $updatedStage,
339 595 'message' => __('Stage has been archived', 'fluent-boards'),
@@ -344,18 +600,20 @@
344 600 }
345 601
346 602 public function restoreStage($board_id, $stage_id)
347 603 {
604 + $board_id = absint($board_id);
605 + $stage_id = absint($stage_id);
606 +
348 607 try {
349 - $stage = Stage::findOrFail($stage_id);
350 - $board = Board::findOrFail($board_id);
608 + $stage = $this->findStageOnBoard($stage_id, $board_id);
351 609
352 - $updatedStage = $this->boardService->restoreStage($board->id, $stage);
610 + $updatedStage = $this->boardService->restoreStage($board_id, $stage);
353 611
354 612 return $this->sendSuccess([
355 - 'success' => true,
613 + 'success' => true,
356 614 'updatedStage' => $updatedStage,
357 - 'message' => __('Stage has been restored', 'fluent-boards')
615 + 'message' => __('Stage has been restored', 'fluent-boards')
358 616 ], 200);
359 617 } catch (\Exception $e) {
360 618 return $this->sendError($e->getMessage(), 400);
361 619 }
@@ -361,32 +619,22 @@
361 619 }
362 620 }
363 621
364 622
365 - public function changePositionOfStage(Request $request, $board_id)
623 + public function repositionStages(Request $request, $board_id)
366 624 {
367 - $changeData = $this->boardSanitizeAndValidate($request->only(['fromPosition', 'toPosition']), [
368 - 'fromPosition' => 'required',
369 - 'toPosition' => 'required',
370 - ]);
371 -
625 + $incomingList = $request->get('list');
626 + if (!is_array($incomingList)) {
627 + $incomingList = [];
628 + }
629 + $incomingList = array_map('intval', $incomingList);
372 630 try {
373 - $this->boardService->changePositionOfStage($board_id, $changeData);
631 + foreach ($incomingList as $stageId) {
632 + $this->findStageOnBoard($stageId, $board_id);
633 + }
374 634
635 + $this->boardService->repositionStages($board_id, $incomingList);
375 636 return $this->sendSuccess([
376 - 'message' => __('Board stage has been updated', 'fluent-boards')
377 - ], 200);
378 - } catch (\Exception $e) {
379 - return $this->sendError($e->getMessage(), 400);
380 - }
381 - }
382 -
383 - public function rePositionStages(Request $request, $board_id)
384 - {
385 - $incomingList = $request->get('list');
386 - try {
387 - $this->boardService->rePositionStages($board_id, $incomingList);
388 - return $this->sendSuccess([
389 637 'message' => __('Stages Reordered', 'fluent-boards'),
390 638 'updatedStages' => $this->stageService->getLastOneMinuteUpdatedStages($board_id)
391 639 ], 200);
392 640 } catch (\Exception $e) {
@@ -404,9 +652,9 @@
404 652 public function delete($board_id)
405 653 {
406 654 try {
407 655 if (!PermissionManager::isAdmin()) {
408 - 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);
409 657 }
410 658 $this->boardService->deleteBoard($board_id);
411 659
412 660 return $this->sendSuccess([
@@ -424,9 +672,12 @@
424 672
425 673 public function getActivities(Request $request, $board_id)
426 674 {
427 675 try {
428 - $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 + ]);
429 680 return $this->sendSuccess([
430 681 'activities' => $activities,
431 682 ], 200);
432 683 } catch (\Exception $e) {
@@ -433,8 +684,11 @@
433 684 return $this->sendError($e->getMessage(), 404);
434 685 }
435 686 }
436 687
688 + /*
689 + * TODO: Refactor this method - for Masiur
690 + */
437 691 public function getBoardUsers($board_id)
438 692 {
439 693 $board = Board::findOrFail($board_id);
440 694
@@ -441,8 +695,11 @@
441 695 $boardObjects = Relation::where('object_type', 'board_user')
442 696 ->where('object_id', $board_id)
443 697 ->get()->keyBy('foreign_id');
444 698
699 + $superAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
700 + ->get()->pluck('object_id')->toArray();
701 +
445 702 $userIds = $boardObjects->pluck('foreign_id')->toArray();
446 703
447 704 $coreUsers = [];
448 705 if ($userIds) {
@@ -461,14 +718,18 @@
461 718 }
462 719
463 720 $boardRelation = $boardObjects[$user->ID] ?? null;
464 721
722 +
465 723 $formattedUsers[] = [
466 724 'ID' => $user->ID,
467 725 'display_name' => $name,
726 + 'user_login' => $user->user_login,
468 727 'email' => $user->user_email,
469 728 'photo' => fluent_boards_user_avatar($user->user_email, $name),
470 - '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')
471 732 ];
472 733 }
473 734
474 735 // order formatted users by display_name
@@ -476,9 +737,9 @@
476 737 return strcmp($a['display_name'], $b['display_name']);
477 738 });
478 739
479 740 $returnData = [
480 - 'users' => Helper::sanitizeUsersArray($formattedUsers),
741 + 'users' => Helper::sanitizeUsersArray($formattedUsers, $board_id),
481 742 'global_admins' => []
482 743 ];
483 744
484 745 if (!PermissionManager::isAdmin(get_current_user_id())) {
@@ -485,16 +746,24 @@
485 746 return $returnData;
486 747 }
487 748
488 749 /*
489 - * 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.
490 751 */
491 - $adminUserIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
752 + $fluentBoardAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
492 753 ->whereNotIn('object_id', $userIds)
493 754 ->get()
494 755 ->pluck('object_id')
495 756 ->toArray();
496 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 +
497 766 if ($adminUserIds) {
498 767 $adminUsers = get_users([
499 768 'include' => $adminUserIds,
500 769 ]);
@@ -511,9 +780,11 @@
511 780 'ID' => $user->ID,
512 781 'display_name' => $name,
513 782 'email' => $user->user_email,
514 783 'photo' => fluent_boards_user_avatar($user->user_email, $name),
515 - 'role' => 'admin'
784 + 'role' => 'admin',
785 + 'is_super' => in_array($user->ID, $superAdminIds),
786 + 'is_wpadmin' => $user->has_cap('manage_options')
516 787 ];
517 788 }
518 789
519 790 // order formatted users by display_name
@@ -520,9 +791,9 @@
520 791 usort($formattedAdminUsers, function ($a, $b) {
521 792 return strcmp($a['display_name'], $b['display_name']);
522 793 });
523 794
524 - $returnData['global_admins'] = Helper::sanitizeUsersArray($formattedAdminUsers);
795 + $returnData['global_admins'] = Helper::sanitizeUsersArray($formattedAdminUsers, $board_id);
525 796 }
526 797
527 798 return $this->sendSuccess($returnData, 200);
528 799 }
@@ -542,18 +813,25 @@
542 813 }
543 814
544 815 public function addMembersInBoard(Request $request, $board_id)
545 816 {
546 - $memberId = $request->getSafe('memberId');
547 - $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);
548 820
549 - if ($isAlreadyMember) {
821 + if ($member === null) {
550 822 return $this->sendError([
823 + 'message' => __('User not found.', 'fluent-boards'),
824 + ], 404);
825 + }
826 +
827 + if (!$member) {
828 + return $this->sendError([
551 829 'message' => __('User already a member', 'fluent-boards'),
552 - ], 304);
830 + ], 409);
553 831 }
554 - $member = $this->boardService->addMembersInBoard($board_id, $memberId);
555 832
833 +
556 834 return [
557 835 'message' => __('Member added successfully', 'fluent-boards'),
558 836 'member' => Helper::sanitizeUserCollections($member)
559 837 ];
@@ -581,11 +859,11 @@
581 859 }
582 860
583 861 public function searchBoards(Request $request)
584 862 {
585 - $per_page = $request->get('per_page', 10);
586 - $search_input = $request->searchInput . trim('');
587 - $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');
588 866
589 867 $currentUserId = get_current_user_id();
590 868
591 869 if (PermissionManager::isAdmin($currentUserId)) {
@@ -627,10 +905,13 @@
627 905 * @return
628 906 */
629 907 public function changeStageView($board_id, $stage_id)
630 908 {
909 + $board_id = absint($board_id);
910 + $stage_id = absint($stage_id);
911 +
631 912 try {
632 - $stage = Stage::findOrFail($stage_id);
913 + $stage = $this->findStageOnBoard($stage_id, $board_id);
633 914 $message = __('The stage is made public!', 'fluent-boards');
634 915 $settings = $stage->settings;
635 916
636 917 if (isset($settings['is_public'])) {
@@ -635,9 +916,9 @@
635 916
636 917 if (isset($settings['is_public'])) {
637 918 if ($settings['is_public']) {
638 919 $settings['is_public'] = false;
639 - $message = __('The stage is made admin only!', 'fluent-boards');
920 + $message = __('The stage is made private!', 'fluent-boards');
640 921 } else {
641 922 $settings['is_public'] = true;
642 923 }
643 924 } else {
@@ -656,24 +937,27 @@
656 937 }
657 938
658 939
659 940 /**
660 - * Set board background image or color
941 + * Set or reset board background image/color.
661 942 * @param \FluentBoards\Framework\Http\Request\Request $request
662 943 * @return
663 944 */
664 945 public function setBoardBackground(Request $request, $board_id)
665 946 {
666 - // sanitize and validate image_url
667 - 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) {
668 955 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
669 - "id" => 'required',
956 + 'id' => 'required|integer',
670 957 'image_url' => 'required|string|url',
671 958 ]);
672 - }
673 -
674 - // sanitize and validate color
675 - if ($request->color) {
959 + } elseif ($request->color) {
676 960 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
677 961 "id" => 'required',
678 962 'color' => 'required',
679 963 ]);
@@ -681,17 +965,22 @@
681 965
682 966 try {
683 967 if (!$board_id) {
684 968 $errorMessage = __('Board id is required', 'fluent-boards');
685 - throw new \Exception($errorMessage, 400);
969 + throw new \Exception(esc_html($errorMessage), 400);
686 970 }
687 971
972 + if (empty($backgroundData)) {
973 + $errorMessage = __('Background data is required', 'fluent-boards');
974 + throw new \Exception(esc_html($errorMessage), 400);
975 + }
976 +
688 977 return $this->sendSuccess([
689 978 'message' => __('Background updated successfully', 'fluent-boards'),
690 979 'background' => $this->boardService->setBoardBackground($backgroundData, $board_id),
691 980 ]);
692 981 } catch (\Exception $e) {
693 - $this->sendError([$e->getMessage(), 400]);
982 + return $this->sendError($e->getMessage(), 400);
694 983 }
695 984 }
696 985
697 986
@@ -701,15 +990,19 @@
701 990 * @param mixed $stage_slug
702 991 * @return $availablePositions as an array
703 992 * @throws \Exception
704 993 */
705 - public function getStageTaskAvailablePositions($board_id, $stage_id)
994 + public function getStageTaskAvailablePositions(Request $request, $board_id, $stage_id)
706 995 {
707 996 try {
708 997 if ($board_id && $stage_id) {
709 - $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);
710 1000 return $this->sendSuccess([
711 - 'availablePositions' => $availablePositions
1001 + 'availablePositions' => $availablePositions['availablePositions'],
1002 + 'moveTargets' => $availablePositions['moveTargets'],
1003 + 'currentMoveTargetKey' => $availablePositions['currentMoveTargetKey'],
1004 + 'defaultMoveTargetKey' => $availablePositions['defaultMoveTargetKey'],
712 1005 ], 200);
713 1006 } else {
714 1007 $message = '';
715 1008 if (!$board_id) {
@@ -717,12 +1010,12 @@
717 1010 }
718 1011 if (!$stage_id) {
719 1012 $message = 'Stage ';
720 1013 }
721 - throw new \Exception($message . 'is required', 400);
1014 + throw new \Exception(esc_html($message . 'is required'), 400);
722 1015 }
723 1016 } catch (\Exception $e) {
724 - $this->sendError([$e->getMessage(), 400]);
1017 + return $this->sendError($e->getMessage(), 400);
725 1018 }
726 1019 }
727 1020
728 1021 public function getAssociateCrmContacts($board_id)
@@ -731,24 +1024,47 @@
731 1024 $contactAssociatedTasks = Task::with('board')->where('board_id', $board_id)
732 1025 ->whereNotNull('crm_contact_id')
733 1026 ->get();
734 1027
735 - $formattedContacts = Collection::make($contactAssociatedTasks)
736 - ->groupBy('crm_contact_id')
737 - ->map(function ($tasks, $contactId) {
738 - $subscriber = Subscriber::find($contactId);
739 - 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) {
740 1057 return null; // Skip if subscriber not found
741 1058 }
742 1059
743 - return [
744 - 'name' => $subscriber->first_name . ' ' . $subscriber->last_name,
745 - 'photo' => $subscriber->photo,
746 - 'email' => $subscriber->email,
747 - 'crm_contact_id' => $contactId,
748 - 'id' => $contactId,
749 - 'tasks' => $tasks,
750 - ];
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;
751 1067 })
752 1068 ->filter()->toArray();
753 1069
754 1070
@@ -769,11 +1085,13 @@
769 1085 'message' => __('Associated Crm Member has been updated', 'fluent-boards'),
770 1086 ], 200);
771 1087 }
772 1088
773 - public function hasDataChanged($board_id)
1089 + public function hasDataChanged(Request $request, $board_id)
774 1090 {
775 - 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);
776 1094 }
777 1095
778 1096 public function createStage(Request $request, $board_id)
779 1097 {
@@ -778,8 +1096,9 @@
778 1096 public function createStage(Request $request, $board_id)
779 1097 {
780 1098 $stageData = $this->stageSanitizeAndValidate($request->all(), [
781 1099 'title' => 'required|string',
1100 + 'position' => 'nullable|numeric'
782 1101 ]);
783 1102
784 1103 $board = Board::find($board_id);
785 1104 $stage = $this->stageService->createStage($stageData, $board_id);
@@ -793,28 +1112,29 @@
793 1112 'message' => __('stage has been created', 'fluent-boards'),
794 1113 ];
795 1114 }
796 1115
797 - public function sortStageTasks(Request $request, $board_id, $stage_id)
1116 + public function moveAllTasks(Request $request, $board_id)
798 1117 {
799 - $sort = $request->getSafe('sort', 'sanitize_text_field');
1118 + $oldStageId = $request->getSafe('oldStageId', 'intval');
1119 + $newStageId = $request->getSafe('newStageId', 'intval');
800 1120
801 - $updatedTasks = $this->stageService->sortStageTasks($sort, $stage_id);
802 - return [
803 - 'message' => __('Tasks has been sorted', 'fluent-boards'),
804 - 'updatedTasks' => $updatedTasks,
805 - ];
806 - }
1121 + if (!$oldStageId || !$newStageId) {
1122 + return $this->sendError(__('Invalid stage IDs provided', 'fluent-boards'), 400);
1123 + }
807 1124
808 - public function moveAllTasks(Request $request, $board_id)
809 - {
810 - $oldStageId = $request->getSafe('oldStageId');
811 - $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();
812 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 +
813 1133 $updates = $this->stageService->moveAllTasks($oldStageId, $newStageId, $board_id);
814 1134
815 1135 return [
816 - 'message' => __('Tasks has been Moved', 'fluent-boards'),
1136 + 'message' => __('Tasks have been moved', 'fluent-boards'),
817 1137 'updatedTasks' => $updates,
818 1138 ];
819 1139
820 1140 }
@@ -820,50 +1140,87 @@
820 1140 }
821 1141
822 1142 public function archiveAllTasksInStage($board_id, $stage_id)
823 1143 {
824 - $updates = $this->stageService->archiveAllTasksInStage($stage_id);
825 - return [
826 - 'message' => __('Tasks has been archived', 'fluent-boards'),
827 - 'updatedTasks' => $updates,
828 - ];
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 + }
829 1158 }
830 1159
831 1160 public function getAssociatedBoards(Request $request, $associated_id)
832 1161 {
833 - $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 +
834 1174 return [
835 1175 'boards' => $associatedBoards,
836 1176 ];
837 1177 }
838 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 +
839 1190 public function duplicateBoard(Request $request, $board_id)
840 1191 {
841 1192 $boardData = $this->taskSanitizeAndValidate($request->get('board'), [
842 1193 'title' => 'required|string'
843 1194 ]);
1195 +
1196 + $boardData['source_board_id'] = $board_id;
1197 +
844 1198 $isWithLabels = $request->getSafe('isWithLabels');
845 1199 $isWithTasks = $request->getSafe('isWithTasks');
1200 + $isWithTemplates = $request->getSafe('isWithTemplates');
846 1201
847 1202 try {
848 1203 if(!PermissionManager::isAdmin()) {
849 1204 $errorMessage = __('You do not have permission to duplicate board', 'fluent-boards');
850 - throw new \Exception($errorMessage, 400);
1205 + throw new \Exception(esc_html($errorMessage), 400);
851 1206 }
852 1207 //create board
853 1208 $newBoard = $this->boardService->copyBoard($boardData);
854 1209
855 1210 //label copy
1211 + $labelMap = [];
1212 +
856 1213 if ($isWithLabels == 'yes') {
857 - $this->labelService->copyLabelsOfBoard($board_id, $newBoard);
1214 + $labelMap = $this->labelService->copyLabelsOfBoard($board_id, $newBoard);
858 1215 }
859 1216
860 1217 //stage copy
861 - $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id);
1218 + $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id, $isWithTemplates);
862 1219
863 1220 //copy tasks of selected stages
864 1221 if ($isWithTasks == 'yes') {
865 - $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard);
1222 + $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap,$isWithTemplates);
866 1223 }
867 1224
868 1225 return $this->sendSuccess([
869 1226 'board' => $newBoard,
@@ -875,11 +1232,18 @@
875 1232
876 1233 public function importFromBoard(Request $request, $board_id)
877 1234 {
878 1235 $selectedStages = $request->getSafe('selectedStages');
1236 + $position = $request->getSafe('position', 'intval');
879 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 +
880 1244 try {
881 - $this->stageService->importStagesFromBoard($board_id, $selectedStages);
1245 + $this->stageService->importStagesFromBoard($board_id, $selectedStages, $position);
882 1246
883 1247 return $this->sendSuccess([
884 1248 'message' => __('Import successfully', 'fluent-boards'),
885 1249 ], 200);
@@ -894,7 +1258,242 @@
894 1258 return [
895 1259 'solidColors' => Constant::BOARD_BACKGROUND_DEFAULT_SOLID_COLORS,
896 1260 'gradients' => Constant::BOARD_BACKGROUND_DEFAULT_GRADIENT_COLORS
897 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;
898 1497 }
899 1498
900 1499 }