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