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