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 +736 -135 1.23trunk 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,14 +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 - if(!defined('FLUENT_ROADMAP')) {
58 - $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->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 + }
59 78 } else {
60 - $relatedBoardsQuery = Board::whereNull('archived_at')->byAccessUser($userId);
79 + if ($option == 'archived') {
80 + $relatedBoardsQuery = Board::whereNotNull('archived_at')->byAccessUser($userId);
81 + } else {
82 + $relatedBoardsQuery = Board::whereNull('archived_at')->byAccessUser($userId);
83 + }
61 84 }
62 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
63 108 if (!empty($searchInput)) {
64 109 $relatedBoardsQuery = $relatedBoardsQuery->where('title', 'like', '%' . $searchInput . '%');
65 110 }
66 111
@@ -69,17 +114,59 @@
69 114 ->with('stages', 'users')
70 115 ->paginate($per_page);
71 116
72 117 foreach ($relatedBoards as $relatedBoard) {
118 + $relatedBoard->description = DescriptionMarkdownConverter::normalize($relatedBoard->description);
73 119 $relatedBoard->users = Helper::sanitizeUserCollections($relatedBoard->users);
120 + $relatedBoard->is_pinned = $this->boardService->isPinned($relatedBoard->id);
74 121 }
75 122
76 - return $this->sendSuccess([
77 - 'boards' => $relatedBoards
78 - ], 200);
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);
79 137 }
80 138
81 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 + /**
82 169 * Get the list of boards and their associated stages for the current user.
83 170 *
84 171 * @param \FluentBoards\Framework\Http\Request\Request $request
85 172 * @return \WP_REST_Response
@@ -85,30 +172,56 @@
85 172 * @return \WP_REST_Response
86 173 */
87 174 public function getBoardsList(Request $request)
88 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 +
191 + return $this->sendSuccess([
192 + 'boards' => $relatedBoards,
193 + 'all_stages' => $stages,
194 + ], 200);
195 + }
196 + public function getOnlyBoardsByUser(Request $request)
197 + {
89 198 try {
90 199 $userId = get_current_user_id();
91 200
92 - // Query to fetch boards that are not archived and accessible by the user
93 - // Check if the FLUENT_ROADMAP constant is defined
94 - if (!defined('FLUENT_ROADMAP')) {
95 - $relatedBoardsQuery = Board::whereNull('archived_at')->where('type', 'to-do')->byAccessUser($userId);
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);
96 207 } else {
97 - $relatedBoardsQuery = Board::whereNull('archived_at')->byAccessUser($userId);
208 + $relatedBoardsQuery = Board::whereNull('archived_at')->excludeTemplates()->byAccessUser($userId);
98 209 }
99 210
100 - $relatedBoards = $relatedBoardsQuery->with('stages')->get();
211 + if (!empty($searchInput)) {
212 + $relatedBoardsQuery = $relatedBoardsQuery->where('title', 'like', '%' . $searchInput . '%');
213 + }
101 214
102 - // Fetch the stages associated with the boards
103 - $stages = Stage::whereIn('board_id', $relatedBoards->pluck('id'))->where('archived_at', null)->get();
215 + $relatedBoards = $relatedBoardsQuery->orderBy('created_at', 'DESC')->get();
104 216
105 217 return $this->sendSuccess([
106 - 'boards' => $relatedBoards,
107 - 'all_stages' => $stages,
108 - ], 200);
218 + 'boards' => $relatedBoards
219 + ]);
109 220 } catch (\Exception $e) {
110 - return $this->sendError($e->getMessage(), 404);
221 + return $this->sendError([
222 + 'message' => $e->getMessage()
223 + ]);
111 224 }
112 225 }
113 226
114 227 public function getRecentBoards()
@@ -115,9 +228,12 @@
115 228 {
116 229 $boards = $this->boardService->getRecentBoards();
117 230
118 231 if (!$boards || $boards->isEmpty()) {
119 - $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())
120 236 ->limit(4)
121 237 ->withCount('completedTasks')
122 238 ->with(['stages', 'users'])
123 239 ->get();
@@ -124,8 +240,9 @@
124 240 }
125 241
126 242 foreach ($boards as $board) {
127 243 $board->users = Helper::sanitizeUserCollections($board->users);
244 + $board->is_pinned = $this->boardService->isPinned($board->id);
128 245 }
129 246
130 247 return [
131 248 'boards' => $boards,
@@ -140,9 +257,9 @@
140 257 $boards = $this->boardService->getBoardsByType($type);
141 258
142 259 return $this->sendSuccess([
143 260 'boards' => $boards,
144 - ], 200);
261 + ]);
145 262 }
146 263
147 264 public function createFirstBoard(Request $request)
148 265 {
@@ -153,11 +270,14 @@
153 270 'currency' => 'nullable|string',
154 271 'crm_contact_id' => 'nullable|numeric',
155 272 ]);
156 273
157 - $installFluentCRM = $request->get('withFluentCRM') == 'yes' ? true : false;
274 + $installFluentCRM = $request->getSafe('withFluentCRM', 'sanitize_text_field') == 'yes' ? true : false;
158 275
159 276 $postStages = $request->get('stages');
277 + if (!is_array($postStages)) {
278 + $postStages = [];
279 + }
160 280 $stageData = array();
161 281 foreach ($postStages as $stage) {
162 282 $temp = $this->stageSanitizeAndValidate($stage, [
163 283 'title' => 'required|string',
@@ -167,9 +287,9 @@
167 287
168 288 $taskData = null;
169 289 if ($request->get('task')) {
170 290 $taskData = $this->taskSanitizeAndValidate($request->get('task'), [
171 - 'title' => 'required|string'
291 + 'title' => 'required|string',
172 292 ]);
173 293 }
174 294
175 295 $board = $this->boardService->createBoard($boardData);
@@ -183,9 +303,9 @@
183 303 $this->taskService->createTask($taskData, $board->id);
184 304 }
185 305
186 306 do_action('fluent_boards/board_created', $board);
187 -
307 +
188 308 if ($installFluentCRM && !defined('FLUENTCRM')) {
189 309 InstallService::install('fluent-crm');
190 310 }
191 311
@@ -194,8 +314,21 @@
194 314 'board' => $board,
195 315 ];
196 316 }
197 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 +
198 331 public function create(Request $request)
199 332 {
200 333 $boardData = $this->boardSanitizeAndValidate($request->get('board'), [
201 334 'title' => 'required|string',
@@ -202,17 +335,44 @@
202 335 'description' => 'nullable',
203 336 'type' => 'required|string',
204 337 'currency' => 'nullable|string',
205 338 'crm_contact_id' => 'nullable|numeric',
339 + 'folder_id' => 'nullable',
206 340 ]);
207 341
208 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 +
209 354 $board = $this->boardService->createBoard($boardData);
210 - $this->labelService->createDefaultLabel($board->id);
355 + $this->createBoardLabelsFromRequest($request, $board->id);
356 + $this->addBoardMembersFromRequest($request, $board->id);
211 357 $type = ucfirst($boardData['type']);
358 + $stages = $request->get('stages');
359 + $sanitizedStages = [];
212 360
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 +
213 371 if (isset($boardData['type']) && $boardData['type'] == 'roadmap') {
214 - $this->stageService->createRoadmapStages($board, $request->get('stages'));
372 + $this->stageService->createRoadmapStages($board, $sanitizedStages);
373 + } elseif (!empty($sanitizedStages)) {
374 + $this->stageService->createStages($board, $sanitizedStages);
215 375 } else {
216 376 $this->stageService->createDefaultStages($board);
217 377 }
218 378
@@ -222,37 +382,118 @@
222 382 }
223 383
224 384 do_action('fluent_boards/board_created', $board);
225 385
386 +
387 + if ($folderId) {
388 + $folderService->addBoardToFolder($folderId, [$board->id]);
389 + }
390 +
226 391 $message = __('Board has been created successfully', 'fluent-boards');
227 392
228 - return $this->sendSuccess([
393 + return $this->send([
229 394 'message' => $message,
230 395 'board' => $board,
231 396 ], 201);
232 397 } catch (\Exception $e) {
233 - return $this->sendError($e->getMessage(), 400);
398 + return $this->sendError([
399 + 'message' => $e->getMessage()
400 + ]);
234 401 }
235 402 }
236 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 + */
237 484 public function getArchivedStage(Request $request, $board_id)
238 485 {
239 486 try {
240 - $pagination = $request->noPagination ? true : false;
241 - $per_page = isset($data['per_page']) ? $data['per_page'] : 30;
242 - $page = isset($data['page']) ? $data['page'] : 1;
243 - if ($pagination) {
244 - $stages = Stage::where('board_id', $board_id)
245 - ->whereNotNull('archived_at')
246 - ->orderBy('created_at', 'DESC')
247 - ->get();
248 - } else {
249 - $stages = Stage::where('board_id', $board_id)
250 - ->whereNotNull('archived_at')
251 - ->orderBy('created_at', 'DESC')
252 - ->paginate($per_page, ['*'], 'page', $page);
253 - }
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 + ];
254 493
494 + $stages = $this->stageService->getArchivedStages($sanitizedParams, $board_id);
495 +
255 496 return $this->sendSuccess([
256 497 'stages' => $stages,
257 498 ], 200);
258 499 } catch (\Exception $e) {
@@ -259,17 +500,33 @@
259 500 return $this->sendError($e->getMessage(), 404);
260 501 }
261 502 }
262 503
263 - public function find($board_id)
504 + public function find(Request $request, $board_id)
264 505 {
265 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);
266 509 $board->background = maybe_unserialize($board->background);
267 510 $board->createdOn = $board->created_at->format('Y-m-d');
268 511
269 - $board->load(['users', 'stages', 'labels', 'owner']);
512 + $board->load(['users', 'labels', 'owner']);
270 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 +
271 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 +
272 529 $board->load(['customFields']);
273 530 }
274 531
275 532 $this->boardService->updateRecentBoards($board_id);
@@ -279,17 +536,30 @@
279 536
280 537 $board->users = Helper::sanitizeUserCollections($board->users);
281 538 $board->owner = Helper::sanitizeUserCollections($board->owner);
282 539
540 + $board->is_pinned = $this->boardService->isPinned($board->id);
541 +
283 542 $board = apply_filters('fluent_boards/board_find', $board);
284 543
285 544 return [
286 - 'board' => $board
545 + 'board' => $board,
546 + 'synced_at' => current_time('mysql')
287 547 ];
288 548 }
289 549
290 550 public function update(Request $request, $board_id)
291 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 +
292 562 $boardData = $this->boardSanitizeAndValidate($request->only(['title', 'description']), [
293 563 'title' => 'required|string',
294 564 'description' => 'nullable|string',
295 565 ]);
@@ -294,8 +564,9 @@
294 564 'description' => 'nullable|string',
295 565 ]);
296 566
297 567 $board = Board::findOrFail($board_id);
568 + $boardData['description'] = DescriptionMarkdownConverter::normalize($boardData['description']);
298 569
299 570 $oldBoard = clone $board;
300 571 $board->fill($boardData);
301 572 $board->save();
@@ -302,21 +573,23 @@
302 573
303 574 do_action('fluent_boards/board_updated', $board, $oldBoard);
304 575
305 576 return [
306 - 'stages' => $board->stages()->get(),
307 577 'message' => __('Board has been updated', 'fluent-boards'),
308 578 'board' => $board,
579 + 'stages' => $board->stages()->get(),
309 580 ];
310 581 }
311 582
312 583 public function archiveStage($board_id, $stage_id)
313 584 {
585 + $board_id = absint($board_id);
586 + $stage_id = absint($stage_id);
587 +
314 588 try {
315 - $stage = Stage::findOrFail($stage_id);
316 - $board = Board::findOrFail($stage->board_id);
589 + $stage = $this->findStageOnBoard($stage_id, $board_id);
317 590
318 - $updatedStage = $this->boardService->archiveStage($board->id, $stage);
591 + $updatedStage = $this->boardService->archiveStage($board_id, $stage);
319 592
320 593 return $this->sendSuccess([
321 594 'updatedStage' => $updatedStage,
322 595 'message' => __('Stage has been archived', 'fluent-boards'),
@@ -327,18 +600,20 @@
327 600 }
328 601
329 602 public function restoreStage($board_id, $stage_id)
330 603 {
604 + $board_id = absint($board_id);
605 + $stage_id = absint($stage_id);
606 +
331 607 try {
332 - $stage = Stage::findOrFail($stage_id);
333 - $board = Board::findOrFail($board_id);
608 + $stage = $this->findStageOnBoard($stage_id, $board_id);
334 609
335 - $updatedStage = $this->boardService->restoreStage($board->id, $stage);
610 + $updatedStage = $this->boardService->restoreStage($board_id, $stage);
336 611
337 612 return $this->sendSuccess([
338 - 'success' => true,
613 + 'success' => true,
339 614 'updatedStage' => $updatedStage,
340 - 'message' => __('Stage has been restored', 'fluent-boards')
615 + 'message' => __('Stage has been restored', 'fluent-boards')
341 616 ], 200);
342 617 } catch (\Exception $e) {
343 618 return $this->sendError($e->getMessage(), 400);
344 619 }
@@ -344,32 +619,22 @@
344 619 }
345 620 }
346 621
347 622
348 - public function changePositionOfStage(Request $request, $board_id)
623 + public function repositionStages(Request $request, $board_id)
349 624 {
350 - $changeData = $this->boardSanitizeAndValidate($request->only(['fromPosition', 'toPosition']), [
351 - 'fromPosition' => 'required',
352 - 'toPosition' => 'required',
353 - ]);
354 -
625 + $incomingList = $request->get('list');
626 + if (!is_array($incomingList)) {
627 + $incomingList = [];
628 + }
629 + $incomingList = array_map('intval', $incomingList);
355 630 try {
356 - $this->boardService->changePositionOfStage($board_id, $changeData);
631 + foreach ($incomingList as $stageId) {
632 + $this->findStageOnBoard($stageId, $board_id);
633 + }
357 634
635 + $this->boardService->repositionStages($board_id, $incomingList);
358 636 return $this->sendSuccess([
359 - 'message' => __('Board stage has been updated', 'fluent-boards')
360 - ], 200);
361 - } catch (\Exception $e) {
362 - return $this->sendError($e->getMessage(), 400);
363 - }
364 - }
365 -
366 - public function rePositionStages(Request $request, $board_id)
367 - {
368 - $incomingList = $request->get('list');
369 - try {
370 - $this->boardService->rePositionStages($board_id, $incomingList);
371 - return $this->sendSuccess([
372 637 'message' => __('Stages Reordered', 'fluent-boards'),
373 638 'updatedStages' => $this->stageService->getLastOneMinuteUpdatedStages($board_id)
374 639 ], 200);
375 640 } catch (\Exception $e) {
@@ -387,9 +652,9 @@
387 652 public function delete($board_id)
388 653 {
389 654 try {
390 655 if (!PermissionManager::isAdmin()) {
391 - 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);
392 657 }
393 658 $this->boardService->deleteBoard($board_id);
394 659
395 660 return $this->sendSuccess([
@@ -407,9 +672,12 @@
407 672
408 673 public function getActivities(Request $request, $board_id)
409 674 {
410 675 try {
411 - $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 + ]);
412 680 return $this->sendSuccess([
413 681 'activities' => $activities,
414 682 ], 200);
415 683 } catch (\Exception $e) {
@@ -427,8 +695,11 @@
427 695 $boardObjects = Relation::where('object_type', 'board_user')
428 696 ->where('object_id', $board_id)
429 697 ->get()->keyBy('foreign_id');
430 698
699 + $superAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
700 + ->get()->pluck('object_id')->toArray();
701 +
431 702 $userIds = $boardObjects->pluck('foreign_id')->toArray();
432 703
433 704 $coreUsers = [];
434 705 if ($userIds) {
@@ -447,14 +718,18 @@
447 718 }
448 719
449 720 $boardRelation = $boardObjects[$user->ID] ?? null;
450 721
722 +
451 723 $formattedUsers[] = [
452 724 'ID' => $user->ID,
453 725 'display_name' => $name,
726 + 'user_login' => $user->user_login,
454 727 'email' => $user->user_email,
455 728 'photo' => fluent_boards_user_avatar($user->user_email, $name),
456 - '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')
457 732 ];
458 733 }
459 734
460 735 // order formatted users by display_name
@@ -471,16 +746,24 @@
471 746 return $returnData;
472 747 }
473 748
474 749 /*
475 - * 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.
476 751 */
477 - $adminUserIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
752 + $fluentBoardAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
478 753 ->whereNotIn('object_id', $userIds)
479 754 ->get()
480 755 ->pluck('object_id')
481 756 ->toArray();
482 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 +
483 766 if ($adminUserIds) {
484 767 $adminUsers = get_users([
485 768 'include' => $adminUserIds,
486 769 ]);
@@ -497,9 +780,11 @@
497 780 'ID' => $user->ID,
498 781 'display_name' => $name,
499 782 'email' => $user->user_email,
500 783 'photo' => fluent_boards_user_avatar($user->user_email, $name),
501 - 'role' => 'admin'
784 + 'role' => 'admin',
785 + 'is_super' => in_array($user->ID, $superAdminIds),
786 + 'is_wpadmin' => $user->has_cap('manage_options')
502 787 ];
503 788 }
504 789
505 790 // order formatted users by display_name
@@ -528,18 +813,25 @@
528 813 }
529 814
530 815 public function addMembersInBoard(Request $request, $board_id)
531 816 {
532 - $memberId = $request->getSafe('memberId');
533 - $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);
534 820
535 - if ($isAlreadyMember) {
821 + if ($member === null) {
536 822 return $this->sendError([
823 + 'message' => __('User not found.', 'fluent-boards'),
824 + ], 404);
825 + }
826 +
827 + if (!$member) {
828 + return $this->sendError([
537 829 'message' => __('User already a member', 'fluent-boards'),
538 - ], 304);
830 + ], 409);
539 831 }
540 - $member = $this->boardService->addMembersInBoard($board_id, $memberId);
541 832
833 +
542 834 return [
543 835 'message' => __('Member added successfully', 'fluent-boards'),
544 836 'member' => Helper::sanitizeUserCollections($member)
545 837 ];
@@ -567,11 +859,11 @@
567 859 }
568 860
569 861 public function searchBoards(Request $request)
570 862 {
571 - $per_page = $request->get('per_page', 10);
572 - $search_input = $request->searchInput . trim('');
573 - $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');
574 866
575 867 $currentUserId = get_current_user_id();
576 868
577 869 if (PermissionManager::isAdmin($currentUserId)) {
@@ -613,10 +905,13 @@
613 905 * @return
614 906 */
615 907 public function changeStageView($board_id, $stage_id)
616 908 {
909 + $board_id = absint($board_id);
910 + $stage_id = absint($stage_id);
911 +
617 912 try {
618 - $stage = Stage::findOrFail($stage_id);
913 + $stage = $this->findStageOnBoard($stage_id, $board_id);
619 914 $message = __('The stage is made public!', 'fluent-boards');
620 915 $settings = $stage->settings;
621 916
622 917 if (isset($settings['is_public'])) {
@@ -621,9 +916,9 @@
621 916
622 917 if (isset($settings['is_public'])) {
623 918 if ($settings['is_public']) {
624 919 $settings['is_public'] = false;
625 - $message = __('The stage is made admin only!', 'fluent-boards');
920 + $message = __('The stage is made private!', 'fluent-boards');
626 921 } else {
627 922 $settings['is_public'] = true;
628 923 }
629 924 } else {
@@ -642,24 +937,27 @@
642 937 }
643 938
644 939
645 940 /**
646 - * Set board background image or color
941 + * Set or reset board background image/color.
647 942 * @param \FluentBoards\Framework\Http\Request\Request $request
648 943 * @return
649 944 */
650 945 public function setBoardBackground(Request $request, $board_id)
651 946 {
652 - // sanitize and validate image_url
653 - 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) {
654 955 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
655 - "id" => 'required',
956 + 'id' => 'required|integer',
656 957 'image_url' => 'required|string|url',
657 958 ]);
658 - }
659 -
660 - // sanitize and validate color
661 - if ($request->color) {
959 + } elseif ($request->color) {
662 960 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
663 961 "id" => 'required',
664 962 'color' => 'required',
665 963 ]);
@@ -667,17 +965,22 @@
667 965
668 966 try {
669 967 if (!$board_id) {
670 968 $errorMessage = __('Board id is required', 'fluent-boards');
671 - throw new \Exception($errorMessage, 400);
969 + throw new \Exception(esc_html($errorMessage), 400);
672 970 }
673 971
972 + if (empty($backgroundData)) {
973 + $errorMessage = __('Background data is required', 'fluent-boards');
974 + throw new \Exception(esc_html($errorMessage), 400);
975 + }
976 +
674 977 return $this->sendSuccess([
675 978 'message' => __('Background updated successfully', 'fluent-boards'),
676 979 'background' => $this->boardService->setBoardBackground($backgroundData, $board_id),
677 980 ]);
678 981 } catch (\Exception $e) {
679 - $this->sendError([$e->getMessage(), 400]);
982 + return $this->sendError($e->getMessage(), 400);
680 983 }
681 984 }
682 985
683 986
@@ -687,15 +990,19 @@
687 990 * @param mixed $stage_slug
688 991 * @return $availablePositions as an array
689 992 * @throws \Exception
690 993 */
691 - public function getStageTaskAvailablePositions($board_id, $stage_id)
994 + public function getStageTaskAvailablePositions(Request $request, $board_id, $stage_id)
692 995 {
693 996 try {
694 997 if ($board_id && $stage_id) {
695 - $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);
696 1000 return $this->sendSuccess([
697 - 'availablePositions' => $availablePositions
1001 + 'availablePositions' => $availablePositions['availablePositions'],
1002 + 'moveTargets' => $availablePositions['moveTargets'],
1003 + 'currentMoveTargetKey' => $availablePositions['currentMoveTargetKey'],
1004 + 'defaultMoveTargetKey' => $availablePositions['defaultMoveTargetKey'],
698 1005 ], 200);
699 1006 } else {
700 1007 $message = '';
701 1008 if (!$board_id) {
@@ -703,12 +1010,12 @@
703 1010 }
704 1011 if (!$stage_id) {
705 1012 $message = 'Stage ';
706 1013 }
707 - throw new \Exception($message . 'is required', 400);
1014 + throw new \Exception(esc_html($message . 'is required'), 400);
708 1015 }
709 1016 } catch (\Exception $e) {
710 - $this->sendError([$e->getMessage(), 400]);
1017 + return $this->sendError($e->getMessage(), 400);
711 1018 }
712 1019 }
713 1020
714 1021 public function getAssociateCrmContacts($board_id)
@@ -717,24 +1024,47 @@
717 1024 $contactAssociatedTasks = Task::with('board')->where('board_id', $board_id)
718 1025 ->whereNotNull('crm_contact_id')
719 1026 ->get();
720 1027
721 - $formattedContacts = Collection::make($contactAssociatedTasks)
722 - ->groupBy('crm_contact_id')
723 - ->map(function ($tasks, $contactId) {
724 - $subscriber = Subscriber::find($contactId);
725 - 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) {
726 1057 return null; // Skip if subscriber not found
727 1058 }
728 1059
729 - return [
730 - 'name' => $subscriber->first_name . ' ' . $subscriber->last_name,
731 - 'photo' => $subscriber->photo,
732 - 'email' => $subscriber->email,
733 - 'crm_contact_id' => $contactId,
734 - 'id' => $contactId,
735 - 'tasks' => $tasks,
736 - ];
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;
737 1067 })
738 1068 ->filter()->toArray();
739 1069
740 1070
@@ -755,11 +1085,13 @@
755 1085 'message' => __('Associated Crm Member has been updated', 'fluent-boards'),
756 1086 ], 200);
757 1087 }
758 1088
759 - public function hasDataChanged($board_id)
1089 + public function hasDataChanged(Request $request, $board_id)
760 1090 {
761 - 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);
762 1094 }
763 1095
764 1096 public function createStage(Request $request, $board_id)
765 1097 {
@@ -764,8 +1096,9 @@
764 1096 public function createStage(Request $request, $board_id)
765 1097 {
766 1098 $stageData = $this->stageSanitizeAndValidate($request->all(), [
767 1099 'title' => 'required|string',
1100 + 'position' => 'nullable|numeric'
768 1101 ]);
769 1102
770 1103 $board = Board::find($board_id);
771 1104 $stage = $this->stageService->createStage($stageData, $board_id);
@@ -781,15 +1114,27 @@
781 1114 }
782 1115
783 1116 public function moveAllTasks(Request $request, $board_id)
784 1117 {
785 - $oldStageId = $request->getSafe('oldStageId');
786 - $newStageId = $request->getSafe('newStageId');
1118 + $oldStageId = $request->getSafe('oldStageId', 'intval');
1119 + $newStageId = $request->getSafe('newStageId', 'intval');
787 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 +
788 1133 $updates = $this->stageService->moveAllTasks($oldStageId, $newStageId, $board_id);
789 1134
790 1135 return [
791 - 'message' => __('Tasks has been Moved', 'fluent-boards'),
1136 + 'message' => __('Tasks have been moved', 'fluent-boards'),
792 1137 'updatedTasks' => $updates,
793 1138 ];
794 1139
795 1140 }
@@ -795,35 +1140,70 @@
795 1140 }
796 1141
797 1142 public function archiveAllTasksInStage($board_id, $stage_id)
798 1143 {
799 - $updates = $this->stageService->archiveAllTasksInStage($stage_id);
800 - return [
801 - 'message' => __('Tasks has been archived', 'fluent-boards'),
802 - 'updatedTasks' => $updates,
803 - ];
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 + }
804 1158 }
805 1159
806 1160 public function getAssociatedBoards(Request $request, $associated_id)
807 1161 {
808 - $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 +
809 1174 return [
810 1175 'boards' => $associatedBoards,
811 1176 ];
812 1177 }
813 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 +
814 1190 public function duplicateBoard(Request $request, $board_id)
815 1191 {
816 1192 $boardData = $this->taskSanitizeAndValidate($request->get('board'), [
817 1193 'title' => 'required|string'
818 1194 ]);
1195 +
1196 + $boardData['source_board_id'] = $board_id;
1197 +
819 1198 $isWithLabels = $request->getSafe('isWithLabels');
820 1199 $isWithTasks = $request->getSafe('isWithTasks');
1200 + $isWithTemplates = $request->getSafe('isWithTemplates');
821 1201
822 1202 try {
823 1203 if(!PermissionManager::isAdmin()) {
824 1204 $errorMessage = __('You do not have permission to duplicate board', 'fluent-boards');
825 - throw new \Exception($errorMessage, 400);
1205 + throw new \Exception(esc_html($errorMessage), 400);
826 1206 }
827 1207 //create board
828 1208 $newBoard = $this->boardService->copyBoard($boardData);
829 1209
@@ -834,13 +1214,13 @@
834 1214 $labelMap = $this->labelService->copyLabelsOfBoard($board_id, $newBoard);
835 1215 }
836 1216
837 1217 //stage copy
838 - $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id);
1218 + $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id, $isWithTemplates);
839 1219
840 1220 //copy tasks of selected stages
841 1221 if ($isWithTasks == 'yes') {
842 - $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap);
1222 + $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap,$isWithTemplates);
843 1223 }
844 1224
845 1225 return $this->sendSuccess([
846 1226 'board' => $newBoard,
@@ -852,11 +1232,18 @@
852 1232
853 1233 public function importFromBoard(Request $request, $board_id)
854 1234 {
855 1235 $selectedStages = $request->getSafe('selectedStages');
1236 + $position = $request->getSafe('position', 'intval');
856 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 +
857 1244 try {
858 - $this->stageService->importStagesFromBoard($board_id, $selectedStages);
1245 + $this->stageService->importStagesFromBoard($board_id, $selectedStages, $position);
859 1246
860 1247 return $this->sendSuccess([
861 1248 'message' => __('Import successfully', 'fluent-boards'),
862 1249 ], 200);
@@ -892,7 +1279,221 @@
892 1279 return [
893 1280 'message' => __('Board has been updated', 'fluent-boards'),
894 1281 'board' => apply_filters('fluent_boards/board_find', $board)
895 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;
896 1497 }
897 1498
898 1499 }