PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.1.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.1.0
2.1.0 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 All 42 releases
← All changes | app/Http/Controllers/BoardController.php +701 -135 1.352.1.0 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,46 @@
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 +
354 + $this->validateRequestedLabelPresets($request->get('labels'));
355 +
233 356 $board = $this->boardService->createBoard($boardData);
234 - $this->labelService->createDefaultLabel($board->id);
357 + $this->createBoardLabelsFromRequest($request, $board->id);
358 + $this->addBoardMembersFromRequest($request, $board->id);
235 359 $type = ucfirst($boardData['type']);
360 + $stages = $request->get('stages');
361 + $sanitizedStages = [];
236 362
363 + if (is_array($stages) && !empty($stages)) {
364 + foreach ($stages as $stage) {
365 + $sanitizedStages[] = $this->stageSanitizeAndValidate($stage, [
366 + 'title' => 'required|string',
367 + 'slug' => 'nullable|string',
368 + 'position' => 'nullable|numeric'
369 + ]);
370 + }
371 + }
372 +
237 373 if (isset($boardData['type']) && $boardData['type'] == 'roadmap') {
238 - $this->stageService->createRoadmapStages($board, $request->get('stages'));
374 + $this->stageService->createRoadmapStages($board, $sanitizedStages);
375 + } elseif (!empty($sanitizedStages)) {
376 + $this->stageService->createStages($board, $sanitizedStages);
239 377 } else {
240 378 $this->stageService->createDefaultStages($board);
241 379 }
242 380
@@ -246,37 +384,151 @@
246 384 }
247 385
248 386 do_action('fluent_boards/board_created', $board);
249 387
388 +
389 + if ($folderId) {
390 + $folderService->addBoardToFolder($folderId, [$board->id]);
391 + }
392 +
250 393 $message = __('Board has been created successfully', 'fluent-boards');
251 394
252 - return $this->sendSuccess([
395 + return $this->send([
253 396 'message' => $message,
254 397 'board' => $board,
255 398 ], 201);
256 399 } catch (\Exception $e) {
257 - return $this->sendError($e->getMessage(), 400);
400 + return $this->sendError([
401 + 'message' => $e->getMessage()
402 + ]);
258 403 }
259 404 }
260 405
406 + private function sanitizeCreateBoardBackground($background)
407 + {
408 + if (!is_array($background) || empty($background['id'])) {
409 + return '';
410 + }
411 +
412 + $backgroundId = sanitize_text_field($background['id']);
413 +
414 + // Only accept ids from the curated solid/gradient palettes and always
415 + // persist the canonical value from the constant (never the client-supplied
416 + // color) so arbitrary CSS can't be stored and later rendered into a style.
417 + $allowedBackgrounds = [];
418 + foreach (array_merge(
419 + Constant::BOARD_BACKGROUND_DEFAULT_SOLID_COLORS,
420 + Constant::BOARD_BACKGROUND_DEFAULT_GRADIENT_COLORS
421 + ) as $option) {
422 + if (isset($option['id'], $option['value'])) {
423 + $allowedBackgrounds[$option['id']] = $option['value'];
424 + }
425 + }
426 +
427 + if (!isset($allowedBackgrounds[$backgroundId])) {
428 + return '';
429 + }
430 +
431 + return [
432 + 'id' => $backgroundId,
433 + 'color' => $allowedBackgrounds[$backgroundId],
434 + 'is_image' => false,
435 + 'image_url' => null,
436 + ];
437 + }
438 +
439 + private function createBoardLabelsFromRequest(Request $request, $boardId)
440 + {
441 + $labels = $request->get('labels');
442 +
443 + if (!is_array($labels)) {
444 + $this->labelService->createDefaultLabel($boardId);
445 + return;
446 + }
447 +
448 + foreach ($labels as $label) {
449 + $labelData = Helper::sanitizeLabel((array) $label);
450 +
451 + if (empty($labelData['label']) && empty($labelData['bg_color']) && empty($labelData['color_preset'])) {
452 + continue;
453 + }
454 +
455 + $labelPayload = [
456 + 'label' => $labelData['label'] ?? '',
457 + 'bg_color' => $labelData['bg_color'] ?? '#f3f4f6',
458 + 'color' => $labelData['color'] ?? '#1B2533',
459 + ];
460 +
461 + if (array_key_exists('color_preset', $labelData)) {
462 + $labelPayload['color_preset'] = $labelData['color_preset'];
463 + }
464 +
465 + $this->labelService->createLabel($labelPayload, $boardId);
466 + }
467 + }
468 +
469 + /**
470 + * Reject unsupported label preset ids before creating any board records.
471 + *
472 + * @param mixed $labels
473 + * @return void
474 + * @throws \Exception
475 + */
476 + private function validateRequestedLabelPresets($labels)
477 + {
478 + if (!is_array($labels)) {
479 + return;
480 + }
481 +
482 + foreach ($labels as $label) {
483 + $labelData = Helper::sanitizeLabel((array) $label);
484 + $presetId = $labelData[Constant::LABEL_COLOR_PRESET_SETTING] ?? null;
485 +
486 + if ($presetId === null || $presetId === '') {
487 + continue;
488 + }
489 +
490 + if (!is_string($presetId) || !Constant::getLabelColorPreset($presetId)) {
491 + throw new \Exception(esc_html__('Invalid label color preset', 'fluent-boards'));
492 + }
493 + }
494 + }
495 +
496 + private function addBoardMembersFromRequest(Request $request, $boardId)
497 + {
498 + $memberIds = $request->get('member_ids');
499 +
500 + if (!is_array($memberIds)) {
501 + return;
502 + }
503 +
504 + $memberIds = array_filter(array_unique(array_map('intval', $memberIds)));
505 + $currentUserId = get_current_user_id();
506 +
507 + foreach ($memberIds as $memberId) {
508 + if ($memberId === $currentUserId) {
509 + continue;
510 + }
511 +
512 + $this->boardService->addMembersInBoard($boardId, $memberId);
513 + }
514 + }
515 +
516 + /**
517 + * Get archived stages for a board with optional pagination and archive actor metadata.
518 + */
261 519 public function getArchivedStage(Request $request, $board_id)
262 520 {
263 521 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 - }
522 + $board_id = absint($board_id);
523 + $sanitizedParams = [
524 + 'noPagination' => $request->getSafe('noPagination', 'boolval', false),
525 + 'per_page' => $request->getSafe('per_page', 'intval', 30),
526 + 'page' => $request->getSafe('page', 'intval', 1),
527 + ];
278 528
529 + $stages = $this->stageService->getArchivedStages($sanitizedParams, $board_id);
530 +
279 531 return $this->sendSuccess([
280 532 'stages' => $stages,
281 533 ], 200);
282 534 } catch (\Exception $e) {
@@ -283,17 +535,33 @@
283 535 return $this->sendError($e->getMessage(), 404);
284 536 }
285 537 }
286 538
287 - public function find($board_id)
539 + public function find(Request $request, $board_id)
288 540 {
289 541 $board = Board::findOrFail($board_id);
542 + $board->description = DescriptionMarkdownConverter::normalize($board->description);
543 + $includeArchived = filter_var($request->get('include_archived', false), FILTER_VALIDATE_BOOLEAN);
290 544 $board->background = maybe_unserialize($board->background);
291 545 $board->createdOn = $board->created_at->format('Y-m-d');
292 546
293 - $board->load(['users', 'stages', 'labels', 'owner']);
547 + $board->load(['users', 'labels', 'owner']);
294 548
549 + if ($includeArchived) {
550 + $board->stages = Stage::where('board_id', $board_id)
551 + ->orderBy('position', 'asc')
552 + ->get();
553 + } else {
554 + $board->load('stages');
555 + }
556 +
295 557 if (defined('FLUENT_BOARDS_PRO')){
558 + $customFiledPositionMeta = $board->getMetaByKey('custom_field_positions');
559 + if(!$customFiledPositionMeta) {
560 + (new CustomFieldService())->reIndexCustomFieldPositions($board_id);
561 + $board->updateMeta('custom_field_positions', 'yes');
562 + }
563 +
296 564 $board->load(['customFields']);
297 565 }
298 566
299 567 $this->boardService->updateRecentBoards($board_id);
@@ -299,21 +567,35 @@
299 567 $this->boardService->updateRecentBoards($board_id);
300 568
301 569 $board->labelColor = Constant::TRELLO_COLOR_MAP;
302 570 $board->labelColorText = Constant::TEXT_COLOR_MAP;
571 + $board->labelColorPresets = Constant::LABEL_COLOR_PRESETS;
303 572
304 573 $board->users = Helper::sanitizeUserCollections($board->users);
305 574 $board->owner = Helper::sanitizeUserCollections($board->owner);
306 575
576 + $board->is_pinned = $this->boardService->isPinned($board->id);
577 +
307 578 $board = apply_filters('fluent_boards/board_find', $board);
308 579
309 580 return [
310 - 'board' => $board
581 + 'board' => $board,
582 + 'synced_at' => current_time('mysql')
311 583 ];
312 584 }
313 585
314 586 public function update(Request $request, $board_id)
315 587 {
588 + // Board identity (title/description) is manager-only. This action shares the
589 + // `update` name with CommentController@update under the same policy group, so the
590 + // guard lives here rather than in a SingleBoardPolicy::update() method that would
591 + // also block ordinary members from editing their own comments.
592 + if (!PermissionManager::isBoardManager(absint($board_id))) {
593 + return $this->sendError([
594 + 'message' => __('You do not have permission to edit this board.', 'fluent-boards'),
595 + ], 403);
596 + }
597 +
316 598 $boardData = $this->boardSanitizeAndValidate($request->only(['title', 'description']), [
317 599 'title' => 'required|string',
318 600 'description' => 'nullable|string',
319 601 ]);
@@ -318,8 +600,9 @@
318 600 'description' => 'nullable|string',
319 601 ]);
320 602
321 603 $board = Board::findOrFail($board_id);
604 + $boardData['description'] = DescriptionMarkdownConverter::normalize($boardData['description']);
322 605
323 606 $oldBoard = clone $board;
324 607 $board->fill($boardData);
325 608 $board->save();
@@ -326,21 +609,23 @@
326 609
327 610 do_action('fluent_boards/board_updated', $board, $oldBoard);
328 611
329 612 return [
330 - 'stages' => $board->stages()->get(),
331 613 'message' => __('Board has been updated', 'fluent-boards'),
332 614 'board' => $board,
615 + 'stages' => $board->stages()->get(),
333 616 ];
334 617 }
335 618
336 619 public function archiveStage($board_id, $stage_id)
337 620 {
621 + $board_id = absint($board_id);
622 + $stage_id = absint($stage_id);
623 +
338 624 try {
339 - $stage = Stage::findOrFail($stage_id);
340 - $board = Board::findOrFail($stage->board_id);
625 + $stage = $this->findStageOnBoard($stage_id, $board_id);
341 626
342 - $updatedStage = $this->boardService->archiveStage($board->id, $stage);
627 + $updatedStage = $this->boardService->archiveStage($board_id, $stage);
343 628
344 629 return $this->sendSuccess([
345 630 'updatedStage' => $updatedStage,
346 631 'message' => __('Stage has been archived', 'fluent-boards'),
@@ -351,18 +636,20 @@
351 636 }
352 637
353 638 public function restoreStage($board_id, $stage_id)
354 639 {
640 + $board_id = absint($board_id);
641 + $stage_id = absint($stage_id);
642 +
355 643 try {
356 - $stage = Stage::findOrFail($stage_id);
357 - $board = Board::findOrFail($board_id);
644 + $stage = $this->findStageOnBoard($stage_id, $board_id);
358 645
359 - $updatedStage = $this->boardService->restoreStage($board->id, $stage);
646 + $updatedStage = $this->boardService->restoreStage($board_id, $stage);
360 647
361 648 return $this->sendSuccess([
362 - 'success' => true,
649 + 'success' => true,
363 650 'updatedStage' => $updatedStage,
364 - 'message' => __('Stage has been restored', 'fluent-boards')
651 + 'message' => __('Stage has been restored', 'fluent-boards')
365 652 ], 200);
366 653 } catch (\Exception $e) {
367 654 return $this->sendError($e->getMessage(), 400);
368 655 }
@@ -368,32 +655,22 @@
368 655 }
369 656 }
370 657
371 658
372 - public function changePositionOfStage(Request $request, $board_id)
659 + public function repositionStages(Request $request, $board_id)
373 660 {
374 - $changeData = $this->boardSanitizeAndValidate($request->only(['fromPosition', 'toPosition']), [
375 - 'fromPosition' => 'required',
376 - 'toPosition' => 'required',
377 - ]);
378 -
661 + $incomingList = $request->get('list');
662 + if (!is_array($incomingList)) {
663 + $incomingList = [];
664 + }
665 + $incomingList = array_map('intval', $incomingList);
379 666 try {
380 - $this->boardService->changePositionOfStage($board_id, $changeData);
667 + foreach ($incomingList as $stageId) {
668 + $this->findStageOnBoard($stageId, $board_id);
669 + }
381 670
671 + $this->boardService->repositionStages($board_id, $incomingList);
382 672 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 673 'message' => __('Stages Reordered', 'fluent-boards'),
397 674 'updatedStages' => $this->stageService->getLastOneMinuteUpdatedStages($board_id)
398 675 ], 200);
399 676 } catch (\Exception $e) {
@@ -411,9 +688,9 @@
411 688 public function delete($board_id)
412 689 {
413 690 try {
414 691 if (!PermissionManager::isAdmin()) {
415 - throw new \Exception('You do not have permission to delete this board', 400);
692 + throw new \Exception(esc_html__('You do not have permission to delete this board', 'fluent-boards'), 400);
416 693 }
417 694 $this->boardService->deleteBoard($board_id);
418 695
419 696 return $this->sendSuccess([
@@ -431,9 +708,12 @@
431 708
432 709 public function getActivities(Request $request, $board_id)
433 710 {
434 711 try {
435 - $activities = $this->boardService->getActivities($board_id, $request->all());
712 + $activities = $this->boardService->getActivities($board_id, [
713 + 'per_page' => $request->getSafe('per_page', 'intval', 40),
714 + 'page' => $request->getSafe('page', 'intval', 1),
715 + ]);
436 716 return $this->sendSuccess([
437 717 'activities' => $activities,
438 718 ], 200);
439 719 } catch (\Exception $e) {
@@ -501,16 +781,24 @@
501 781 return $returnData;
502 782 }
503 783
504 784 /*
505 - * These are the rest of the admin users who are not in the board
785 + * These are the rest of the Fluent Boards and WordPress admins who are not in the board.
506 786 */
507 - $adminUserIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
787 + $fluentBoardAdminIds = Meta::query()->where('object_type', Constant::FLUENT_BOARD_ADMIN)
508 788 ->whereNotIn('object_id', $userIds)
509 789 ->get()
510 790 ->pluck('object_id')
511 791 ->toArray();
512 792
793 + $wordPressAdminIds = get_users([
794 + 'capability' => 'manage_options',
795 + 'exclude' => $userIds,
796 + 'fields' => 'ID',
797 + ]);
798 +
799 + $adminUserIds = array_values(array_unique(array_map('intval', array_merge($fluentBoardAdminIds, $wordPressAdminIds))));
800 +
513 801 if ($adminUserIds) {
514 802 $adminUsers = get_users([
515 803 'include' => $adminUserIds,
516 804 ]);
@@ -560,19 +848,25 @@
560 848 }
561 849
562 850 public function addMembersInBoard(Request $request, $board_id)
563 851 {
564 - $memberId = $request->getSafe('memberId');
565 - $isViewerOnly = $request->getSafe('isViewerOnly');
566 - $isAlreadyMember = $this->boardService->isAlreadyMember($board_id, $memberId);
852 + $memberId = $request->getSafe('memberId', 'intval');
853 + $isViewerOnly = $request->getSafe('isViewerOnly', 'sanitize_text_field');
854 + $member = $this->boardService->addMembersInBoard($board_id, $memberId, $isViewerOnly);
567 855
568 - if ($isAlreadyMember) {
856 + if ($member === null) {
569 857 return $this->sendError([
858 + 'message' => __('User not found.', 'fluent-boards'),
859 + ], 404);
860 + }
861 +
862 + if (!$member) {
863 + return $this->sendError([
570 864 'message' => __('User already a member', 'fluent-boards'),
571 - ], 304);
865 + ], 409);
572 866 }
573 - $member = $this->boardService->addMembersInBoard($board_id, $memberId, $isViewerOnly);
574 867
868 +
575 869 return [
576 870 'message' => __('Member added successfully', 'fluent-boards'),
577 871 'member' => Helper::sanitizeUserCollections($member)
578 872 ];
@@ -600,11 +894,11 @@
600 894 }
601 895
602 896 public function searchBoards(Request $request)
603 897 {
604 - $per_page = $request->get('per_page', 10);
605 - $search_input = $request->searchInput . trim('');
606 - $type = $request->type;
898 + $per_page = $request->getSafe('per_page', 'intval', 10);
899 + $search_input = $request->getSafe('searchInput', 'sanitize_text_field', '');
900 + $type = $request->getSafe('type', 'sanitize_text_field', 'to-do');
607 901
608 902 $currentUserId = get_current_user_id();
609 903
610 904 if (PermissionManager::isAdmin($currentUserId)) {
@@ -646,10 +940,13 @@
646 940 * @return
647 941 */
648 942 public function changeStageView($board_id, $stage_id)
649 943 {
944 + $board_id = absint($board_id);
945 + $stage_id = absint($stage_id);
946 +
650 947 try {
651 - $stage = Stage::findOrFail($stage_id);
948 + $stage = $this->findStageOnBoard($stage_id, $board_id);
652 949 $message = __('The stage is made public!', 'fluent-boards');
653 950 $settings = $stage->settings;
654 951
655 952 if (isset($settings['is_public'])) {
@@ -654,9 +951,9 @@
654 951
655 952 if (isset($settings['is_public'])) {
656 953 if ($settings['is_public']) {
657 954 $settings['is_public'] = false;
658 - $message = __('The stage is made admin only!', 'fluent-boards');
955 + $message = __('The stage is made private!', 'fluent-boards');
659 956 } else {
660 957 $settings['is_public'] = true;
661 958 }
662 959 } else {
@@ -675,24 +972,27 @@
675 972 }
676 973
677 974
678 975 /**
679 - * Set board background image or color
976 + * Set or reset board background image/color.
680 977 * @param \FluentBoards\Framework\Http\Request\Request $request
681 978 * @return
682 979 */
683 980 public function setBoardBackground(Request $request, $board_id)
684 981 {
685 - // sanitize and validate image_url
686 - if ($request->image_url) {
982 + $backgroundData = [];
983 + $isResetRequest = $request->getSafe('reset', 'rest_sanitize_boolean');
984 +
985 + if ($isResetRequest) {
986 + $backgroundData = [
987 + 'reset' => true,
988 + ];
989 + } elseif ($request->image_url) {
687 990 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
688 - "id" => 'required',
991 + 'id' => 'required|integer',
689 992 'image_url' => 'required|string|url',
690 993 ]);
691 - }
692 -
693 - // sanitize and validate color
694 - if ($request->color) {
994 + } elseif ($request->color) {
695 995 $backgroundData = $this->boardSanitizeAndValidate($request->all(), [
696 996 "id" => 'required',
697 997 'color' => 'required',
698 998 ]);
@@ -700,17 +1000,22 @@
700 1000
701 1001 try {
702 1002 if (!$board_id) {
703 1003 $errorMessage = __('Board id is required', 'fluent-boards');
704 - throw new \Exception($errorMessage, 400);
1004 + throw new \Exception(esc_html($errorMessage), 400);
705 1005 }
706 1006
1007 + if (empty($backgroundData)) {
1008 + $errorMessage = __('Background data is required', 'fluent-boards');
1009 + throw new \Exception(esc_html($errorMessage), 400);
1010 + }
1011 +
707 1012 return $this->sendSuccess([
708 1013 'message' => __('Background updated successfully', 'fluent-boards'),
709 1014 'background' => $this->boardService->setBoardBackground($backgroundData, $board_id),
710 1015 ]);
711 1016 } catch (\Exception $e) {
712 - $this->sendError([$e->getMessage(), 400]);
1017 + return $this->sendError($e->getMessage(), 400);
713 1018 }
714 1019 }
715 1020
716 1021
@@ -720,15 +1025,19 @@
720 1025 * @param mixed $stage_slug
721 1026 * @return $availablePositions as an array
722 1027 * @throws \Exception
723 1028 */
724 - public function getStageTaskAvailablePositions($board_id, $stage_id)
1029 + public function getStageTaskAvailablePositions(Request $request, $board_id, $stage_id)
725 1030 {
726 1031 try {
727 1032 if ($board_id && $stage_id) {
728 - $availablePositions = $this->boardService->getStageTaskAvailablePositions($board_id, $stage_id);
1033 + $taskId = $request->getSafe('task_id', 'intval');
1034 + $availablePositions = $this->boardService->getStageTaskAvailablePositions($board_id, $stage_id, $taskId);
729 1035 return $this->sendSuccess([
730 - 'availablePositions' => $availablePositions
1036 + 'availablePositions' => $availablePositions['availablePositions'],
1037 + 'moveTargets' => $availablePositions['moveTargets'],
1038 + 'currentMoveTargetKey' => $availablePositions['currentMoveTargetKey'],
1039 + 'defaultMoveTargetKey' => $availablePositions['defaultMoveTargetKey'],
731 1040 ], 200);
732 1041 } else {
733 1042 $message = '';
734 1043 if (!$board_id) {
@@ -736,12 +1045,12 @@
736 1045 }
737 1046 if (!$stage_id) {
738 1047 $message = 'Stage ';
739 1048 }
740 - throw new \Exception($message . 'is required', 400);
1049 + throw new \Exception(esc_html($message . 'is required'), 400);
741 1050 }
742 1051 } catch (\Exception $e) {
743 - $this->sendError([$e->getMessage(), 400]);
1052 + return $this->sendError($e->getMessage(), 400);
744 1053 }
745 1054 }
746 1055
747 1056 public function getAssociateCrmContacts($board_id)
@@ -750,24 +1059,47 @@
750 1059 $contactAssociatedTasks = Task::with('board')->where('board_id', $board_id)
751 1060 ->whereNotNull('crm_contact_id')
752 1061 ->get();
753 1062
754 - $formattedContacts = Collection::make($contactAssociatedTasks)
755 - ->groupBy('crm_contact_id')
756 - ->map(function ($tasks, $contactId) {
757 - $subscriber = Subscriber::find($contactId);
758 - if (!$subscriber) {
1063 + $tasksByContact = [];
1064 + foreach ($contactAssociatedTasks as $task) {
1065 + $tasksByContact[absint($task->crm_contact_id)][] = $task;
1066 + }
1067 +
1068 + $boardContactIds = Meta::query()
1069 + ->where('object_id', absint($board_id))
1070 + ->where('object_type', Constant::OBJECT_TYPE_BOARD)
1071 + ->whereIn('key', [
1072 + Constant::BOARD_ASSOCIATED_CRM_CONTACT,
1073 + self::LEGACY_BOARD_ASSOCIATED_CRM_CONTACT,
1074 + ])
1075 + ->pluck('value')
1076 + ->toArray();
1077 + $boardContactIds = array_values(array_unique(array_filter(array_map('absint', $boardContactIds))));
1078 +
1079 + $contactIds = array_values(array_unique(array_filter(array_map('absint', array_merge(
1080 + array_keys($tasksByContact),
1081 + $boardContactIds
1082 + )))));
1083 +
1084 + usort($contactIds, function ($firstContactId, $secondContactId) use ($boardContactIds) {
1085 + return (int) in_array($secondContactId, $boardContactIds, true) - (int) in_array($firstContactId, $boardContactIds, true);
1086 + });
1087 +
1088 + $formattedContacts = Collection::make($contactIds)
1089 + ->map(function ($contactId) use ($tasksByContact, $boardContactIds) {
1090 + $contact = Helper::crm_contact($contactId);
1091 + if (!$contact) {
759 1092 return null; // Skip if subscriber not found
760 1093 }
761 1094
762 - return [
763 - 'name' => $subscriber->first_name . ' ' . $subscriber->last_name,
764 - 'photo' => $subscriber->photo,
765 - 'email' => $subscriber->email,
766 - 'crm_contact_id' => $contactId,
767 - 'id' => $contactId,
768 - 'tasks' => $tasks,
769 - ];
1095 + $tasks = $tasksByContact[$contactId] ?? [];
1096 + $contact['name'] = trim(($contact['first_name'] ?? '') . ' ' . ($contact['last_name'] ?? '')) ?: ($contact['full_name'] ?? $contact['email'] ?? '');
1097 + $contact['crm_contact_id'] = $contactId;
1098 + $contact['is_board_contact'] = in_array($contactId, $boardContactIds, true);
1099 + $contact['tasks'] = $tasks;
1100 +
1101 + return $contact;
770 1102 })
771 1103 ->filter()->toArray();
772 1104
773 1105
@@ -788,11 +1120,13 @@
788 1120 'message' => __('Associated Crm Member has been updated', 'fluent-boards'),
789 1121 ], 200);
790 1122 }
791 1123
792 - public function hasDataChanged($board_id)
1124 + public function hasDataChanged(Request $request, $board_id)
793 1125 {
794 - return $this->boardService->hasDataChanged($board_id);
1126 + $includeArchived = filter_var($request->get('include_archived', false), FILTER_VALIDATE_BOOLEAN);
1127 + $since = $request->getSafe('since', 'sanitize_text_field');
1128 + return $this->boardService->hasDataChanged($board_id, $includeArchived, $since);
795 1129 }
796 1130
797 1131 public function createStage(Request $request, $board_id)
798 1132 {
@@ -797,8 +1131,9 @@
797 1131 public function createStage(Request $request, $board_id)
798 1132 {
799 1133 $stageData = $this->stageSanitizeAndValidate($request->all(), [
800 1134 'title' => 'required|string',
1135 + 'position' => 'nullable|numeric'
801 1136 ]);
802 1137
803 1138 $board = Board::find($board_id);
804 1139 $stage = $this->stageService->createStage($stageData, $board_id);
@@ -814,15 +1149,27 @@
814 1149 }
815 1150
816 1151 public function moveAllTasks(Request $request, $board_id)
817 1152 {
818 - $oldStageId = $request->getSafe('oldStageId');
819 - $newStageId = $request->getSafe('newStageId');
1153 + $oldStageId = $request->getSafe('oldStageId', 'intval');
1154 + $newStageId = $request->getSafe('newStageId', 'intval');
820 1155
1156 + if (!$oldStageId || !$newStageId) {
1157 + return $this->sendError(__('Invalid stage IDs provided', 'fluent-boards'), 400);
1158 + }
1159 +
1160 + // Verify stages exist and belong to the board
1161 + $oldStage = Stage::where('id', $oldStageId)->where('board_id', $board_id)->first();
1162 + $newStage = Stage::where('id', $newStageId)->where('board_id', $board_id)->first();
1163 +
1164 + if (!$oldStage || !$newStage) {
1165 + return $this->sendError(__('One or both stages do not exist or do not belong to this board', 'fluent-boards'), 400);
1166 + }
1167 +
821 1168 $updates = $this->stageService->moveAllTasks($oldStageId, $newStageId, $board_id);
822 1169
823 1170 return [
824 - 'message' => __('Tasks has been Moved', 'fluent-boards'),
1171 + 'message' => __('Tasks have been moved', 'fluent-boards'),
825 1172 'updatedTasks' => $updates,
826 1173 ];
827 1174
828 1175 }
@@ -828,35 +1175,70 @@
828 1175 }
829 1176
830 1177 public function archiveAllTasksInStage($board_id, $stage_id)
831 1178 {
832 - $updates = $this->stageService->archiveAllTasksInStage($stage_id);
833 - return [
834 - 'message' => __('Tasks has been archived', 'fluent-boards'),
835 - 'updatedTasks' => $updates,
836 - ];
1179 + $board_id = absint($board_id);
1180 + $stage_id = absint($stage_id);
1181 +
1182 + try {
1183 + $this->findStageOnBoard($stage_id, $board_id);
1184 + $updates = $this->stageService->archiveAllTasksInStage($stage_id, $board_id);
1185 +
1186 + return [
1187 + 'message' => __('Tasks have been archived', 'fluent-boards'),
1188 + 'updatedTasks' => $updates,
1189 + ];
1190 + } catch (\Exception $e) {
1191 + return $this->sendError($e->getMessage(), 400);
1192 + }
837 1193 }
838 1194
839 1195 public function getAssociatedBoards(Request $request, $associated_id)
840 1196 {
841 - $associatedBoards = $this->boardService->getAssociatedBoards($associated_id);
1197 + if (!$this->currentUserCanReadCrmContacts()) {
1198 + return $this->sendError(esc_html__('You do not have permission to view CRM contact boards', 'fluent-boards'), 403);
1199 + }
1200 +
1201 + $associatedId = absint($associated_id);
1202 +
1203 + if (!$associatedId) {
1204 + return $this->sendError(__('Invalid CRM contact', 'fluent-boards'), 400);
1205 + }
1206 +
1207 + $associatedBoards = $this->boardService->getAssociatedBoards($associatedId, get_current_user_id());
1208 +
842 1209 return [
843 1210 'boards' => $associatedBoards,
844 1211 ];
845 1212 }
846 1213
1214 + private function currentUserCanReadCrmContacts()
1215 + {
1216 + $permissionManager = 'FluentCrm\\App\\Services\\PermissionManager';
1217 +
1218 + if (!class_exists($permissionManager)) {
1219 + return false;
1220 + }
1221 +
1222 + return (bool) $permissionManager::currentUserCan('fcrm_read_contacts');
1223 + }
1224 +
847 1225 public function duplicateBoard(Request $request, $board_id)
848 1226 {
849 1227 $boardData = $this->taskSanitizeAndValidate($request->get('board'), [
850 1228 'title' => 'required|string'
851 1229 ]);
1230 +
1231 + $boardData['source_board_id'] = $board_id;
1232 +
852 1233 $isWithLabels = $request->getSafe('isWithLabels');
853 1234 $isWithTasks = $request->getSafe('isWithTasks');
1235 + $isWithTemplates = $request->getSafe('isWithTemplates');
854 1236
855 1237 try {
856 1238 if(!PermissionManager::isAdmin()) {
857 1239 $errorMessage = __('You do not have permission to duplicate board', 'fluent-boards');
858 - throw new \Exception($errorMessage, 400);
1240 + throw new \Exception(esc_html($errorMessage), 400);
859 1241 }
860 1242 //create board
861 1243 $newBoard = $this->boardService->copyBoard($boardData);
862 1244
@@ -867,13 +1249,13 @@
867 1249 $labelMap = $this->labelService->copyLabelsOfBoard($board_id, $newBoard);
868 1250 }
869 1251
870 1252 //stage copy
871 - $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id);
1253 + $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id, $isWithTemplates);
872 1254
873 1255 //copy tasks of selected stages
874 1256 if ($isWithTasks == 'yes') {
875 - $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap);
1257 + $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap,$isWithTemplates);
876 1258 }
877 1259
878 1260 return $this->sendSuccess([
879 1261 'board' => $newBoard,
@@ -885,11 +1267,18 @@
885 1267
886 1268 public function importFromBoard(Request $request, $board_id)
887 1269 {
888 1270 $selectedStages = $request->getSafe('selectedStages');
1271 + $position = $request->getSafe('position', 'intval');
889 1272
1273 + // Validate and sanitize selectedStages array
1274 + if (!is_array($selectedStages)) {
1275 + $selectedStages = [$selectedStages];
1276 + }
1277 + $selectedStages = array_filter(array_map('intval', $selectedStages));
1278 +
890 1279 try {
891 - $this->stageService->importStagesFromBoard($board_id, $selectedStages);
1280 + $this->stageService->importStagesFromBoard($board_id, $selectedStages, $position);
892 1281
893 1282 return $this->sendSuccess([
894 1283 'message' => __('Import successfully', 'fluent-boards'),
895 1284 ], 200);
@@ -962,7 +1351,184 @@
962 1351 ? 'manager'
963 1352 : ($boardRelation && Arr::has($boardRelation->settings, 'is_viewer_only') && Arr::get($boardRelation->settings, 'is_viewer_only')
964 1353 ? 'viewer'
965 1354 : 'member');
1355 + }
1356 + public function uploadBoardBackground(Request $request,$board_id)
1357 + {
1358 + $file = Arr::get($request->files(), 'file')->toArray();
1359 + (new \FluentBoards\App\Services\UploadService)->validateFile($file);
1360 +
1361 + $uploadInfo = UploadService::handleFileUpload( $request->files(), $board_id);
1362 +
1363 + $fileData = $uploadInfo[0];
1364 + $initialDataData = [
1365 + 'type' => 'url',
1366 + 'url' => '',
1367 + 'name' => '',
1368 + 'size' => 0,
1369 + ];
1370 +
1371 + $attachData = array_merge($initialDataData, $fileData);
1372 + $UrlMeta = [];
1373 + if($attachData['type'] == 'url') {
1374 + $UrlMeta = RemoteUrlParser::parse($attachData['url']);
1375 + }
1376 + $uid = wp_generate_uuid4();
1377 + $fileUploadedData = new Attachment();
1378 + $fileUploadedData->object_id = $board_id;
1379 + $fileUploadedData->object_type = Constant::BOARD_BACKGROUND_IMAGE;
1380 + $fileUploadedData->attachment_type = $attachData['type'];
1381 + $fileUploadedData->title = (new TaskService())->setTitle($attachData['type'], $attachData['name'], $UrlMeta);
1382 + $fileUploadedData->file_path = $attachData['type'] != 'url' ? $attachData['file'] : null;
1383 + $fileUploadedData->full_url = esc_url($attachData['url']);
1384 + $fileUploadedData->file_size = $attachData['size'];
1385 + $fileUploadedData->settings = $attachData['type'] == 'url' ? [
1386 + 'meta' => $UrlMeta
1387 + ] : '';
1388 + $fileUploadedData->driver = 'local';
1389 + $fileUploadedData->file_hash = md5($uid . wp_rand(0, 1000));
1390 + $fileUploadedData->save();
1391 + if(!!defined('FLUENT_BOARDS_PRO_VERSION')) {
1392 + $mediaData = (new AttachmentService())->processMediaData($fileData, $file);
1393 + $fileUploadedData['driver'] = $mediaData['driver'];
1394 + $fileUploadedData['file_path'] = $mediaData['file_path'];
1395 + $fileUploadedData['full_url'] = $mediaData['full_url'];
1396 + $fileUploadedData->save();
1397 + }
1398 +
1399 + $board = Board::find($board_id);
1400 + $oldBackground = $board->background;
1401 + $publicUrl = (new CommentService())->createPublicUrl($fileUploadedData, $board_id);
1402 + $background = [
1403 + 'color' => null,
1404 + 'id' => $fileUploadedData->id,
1405 + 'image_url' => $publicUrl,
1406 + 'is_image' => true,
1407 + ];
1408 + $board->background = $background;
1409 + $board->save();
1410 + do_action('fluent_boards/board_background_updated', $board_id, $oldBackground);
1411 +
1412 + return $this->sendSuccess([
1413 + 'message' => __('Background updated successfully', 'fluent-boards'),
1414 + 'background' => $board->background,
1415 + ]);
1416 + }
1417 +
1418 + public function getPinnedBoards()
1419 + {
1420 + $pinnedBoards = $this->boardService->getPinnedBoards();
1421 +
1422 + return $this->sendSuccess([
1423 + 'pinnedBoards' => $pinnedBoards,
1424 + ], 200);
1425 + }
1426 +
1427 + public function pinBoard($boardId)
1428 + {
1429 + $this->boardService->pinBoard($boardId);
1430 +
1431 + return $this->sendSuccess([
1432 + 'message' => __('The Board has been pinned', 'fluent-boards'),
1433 + ], 200);
1434 + }
1435 +
1436 + public function unpinBoard($boardId)
1437 + {
1438 + $remove = $this->boardService->unpinBoard($boardId);
1439 +
1440 + if (!$remove) {
1441 + return $this->sendError([
1442 + 'message' => __('Board is not pinned', 'fluent-boards'),
1443 + ], 400);
1444 + }
1445 +
1446 + return $this->sendSuccess([
1447 + 'message' => __('Board is removed from pinned boards', 'fluent-boards'),
1448 + ], 200);
1449 + }
1450 +
1451 + public function getBoardFolder($board_id)
1452 + {
1453 + try {
1454 + $folder = $this->boardService->getBoardFolder($board_id);
1455 + return $this->sendSuccess([
1456 + 'folder' => $folder,
1457 + ], 200);
1458 + } catch (\Exception $e) {
1459 + return $this->sendError($e->getMessage(), 400);
1460 + }
1461 + }
1462 +
1463 + public function getBoardMenuItems($board_id)
1464 + {
1465 + try {
1466 + $menuItems = (new BoardMenuHandler())->getMenuItems($board_id);
1467 +
1468 + return $this->sendSuccess([
1469 + 'menu_items' => $menuItems
1470 + ], 200);
1471 + } catch (\Exception $e) {
1472 + return $this->sendError($e->getMessage(), 500);
1473 + }
1474 + }
1475 +
1476 + public function getPublicAccessSettings($board_id)
1477 + {
1478 + $board_id = absint($board_id);
1479 + $board = Board::findOrFail($board_id);
1480 +
1481 + $enabled = (bool) $board->getMetaByKey('public_access_enabled');
1482 + $shortcode = $enabled ? '[fluent_board_public id="' . $board_id . '"]' : '';
1483 +
1484 + return $this->sendSuccess([
1485 + 'enabled' => $enabled,
1486 + 'shortcode' => $shortcode,
1487 + ], 200);
1488 + }
1489 +
1490 + public function togglePublicAccess(Request $request, $board_id)
1491 + {
1492 + $board_id = absint($board_id);
1493 + $board = Board::findOrFail($board_id);
1494 +
1495 + $enabled = filter_var(
1496 + $request->getSafe('enabled', 'sanitize_text_field', false),
1497 + FILTER_VALIDATE_BOOLEAN
1498 + );
1499 +
1500 + $board->updateMeta('public_access_enabled', $enabled ? '1' : '');
1501 +
1502 + $shortcode = $enabled ? '[fluent_board_public id="' . $board_id . '"]' : '';
1503 +
1504 + return $this->sendSuccess([
1505 + 'message' => $enabled
1506 + ? __('Public access has been enabled', 'fluent-boards')
1507 + : __('Public access has been disabled', 'fluent-boards'),
1508 + 'enabled' => $enabled,
1509 + 'shortcode' => $shortcode,
1510 + ], 200);
1511 + }
1512 +
1513 + /**
1514 + * Resolve a stage only when it belongs to the requested board.
1515 + *
1516 + * @param int $stageId
1517 + * @param int $boardId
1518 + * @return Stage
1519 + * @throws \Exception
1520 + */
1521 + private function findStageOnBoard($stageId, $boardId)
1522 + {
1523 + $stage = Stage::where('id', absint($stageId))
1524 + ->where('board_id', absint($boardId))
1525 + ->first();
1526 +
1527 + if (!$stage) {
1528 + throw new \Exception(esc_html__('Stage not found', 'fluent-boards'));
1529 + }
1530 +
1531 + return $stage;
966 1532 }
967 1533
968 1534 }