boardService = $boardService; $this->optionService = $optionService; } public function selectorOptions(Request $request) { try { $optionKey = $request->getSafe('option_key', 'sanitize_text_field'); $search = $request->getSafe('search', 'sanitize_text_field'); $includedIds = $request->getSafe('values'); $boardId = $request->getSafe('board_id', 'intval'); $options = []; if ('users' === $optionKey || 'task_assignees' === $optionKey) { // no ajax/code is designed to handle this eventually will goto else if (!PermissionManager::isBoardManager($boardId)) { throw new \Exception(esc_html__('You do not have permission to access this route', 'fluent-boards')); } if (!defined('FLUENT_BOARDS_PRO')) { // get who has 'manage_options' capability $users = PermissionManager::getAll_WP_Admins($search); } else { // Search by user login, email, and nicename, first_name , last_name $users = Helper::searchWordPressUsers($search); $users = Helper::sanitizeUsersArray($users, $boardId); } $options = $this->addUserDataAsSelectorOption($users); } elseif ('board_create_users' === $optionKey) { if (!PermissionManager::userHasBoardCreationPermission()) { throw new \Exception(esc_html__('You do not have permission to access this route', 'fluent-boards')); } if (!defined('FLUENT_BOARDS_PRO')) { // Bound the lookup: this popover searches on focus (empty query too). $users = PermissionManager::getAll_WP_Admins($search, 20); } else { $users = Helper::searchWordPressUsers($search); } $options = $this->addUserDataAsSelectorOption($users); // Board creation can be delegated to members without `list_users`; // don't leak full account emails to them. $options = $this->maskSelectorEmailsForViewer($options); } elseif ('boards' === $optionKey) { $boards = Board::query() ->byAccessUser(get_current_user_id()) ->when($search, function ($query) use ($search) { // $search is already sanitized with sanitize_text_field above return $query->where('title', 'LIKE', '%' . $search . '%'); })->take(20)->get(); foreach ($boards as $board) { $options[] = [ 'id' => $board->id, 'title' => $board->title, 'left_side_value' => $board->title, 'right_side_value' => $board->slug, ]; } } elseif ('tasks' === $optionKey) { if (!PermissionManager::userHasPermission($boardId)) { throw new \Exception(esc_html__('You do not have permission to access this route', 'fluent-boards')); } // $boardId is already sanitized with intval above $tasks = Task::where('board_id', $boardId) ->whereNull('archived_at') ->whereNull('parent_id') ->when($search, function ($query) use ($search) { // $search is already sanitized with sanitize_text_field above return $query->where('title', 'LIKE', '%' . $search . '%'); })->take(20)->get(); foreach ($tasks as $task) { $options[] = [ 'id' => $task->id, 'title' => $task->title, 'board_id' => $task->board_id, 'subtask_groups' => $task->subtaskGroup ]; } } elseif ('assigned_in_task' == $optionKey) { if (!PermissionManager::userHasPermission($boardId)) { throw new \Exception(esc_html__('You do not have permission to access this route', 'fluent-boards')); } $users = (new BoardService())->getAssigneesByBoard($boardId, $search); $options = $this->addUserDataAsSelectorOption($users); } else { $options = apply_filters('fluent_boards/ajax_options_' . $optionKey, [], $search, $includedIds); } return $this->sendSuccess([ 'options' => $options, ], 200); } catch (\Exception $e) { return $this->sendError($e->getMessage(), 404); } } private function addUserDataAsSelectorOption($users) { $options = []; foreach ($users as $user) { $options[] = [ 'id' => $user->ID, 'email' => $user->user_email, 'name' => $user->display_name ?? $user->user_email, 'title' => $user->display_name . ' (' . $user->user_email . ')', 'photo' => get_avatar_url($user->user_email), ]; } return $options; } /** * Obfuscate emails in selector options for viewers who lack the `list_users` * capability, keeping the current user's own email visible. */ private function maskSelectorEmailsForViewer($options) { if (current_user_can('list_users')) { return $options; } $currentUser = wp_get_current_user(); $currentUserEmail = ($currentUser && isset($currentUser->user_email)) ? $currentUser->user_email : ''; foreach ($options as $index => $option) { $email = $option['email'] ?? ''; if ($email === '' || $email === $currentUserEmail) { continue; } $maskedEmail = Helper::obfuscateEmail($email); // When there's no display name the raw email doubles as the name. if (($option['name'] ?? '') === $email) { $options[$index]['name'] = $maskedEmail; } $options[$index]['email'] = $maskedEmail; $options[$index]['title'] = ($options[$index]['name'] ?? $maskedEmail) . ' (' . $maskedEmail . ')'; } return $options; } public function getCurrentUserPermissions() { try { $currentUserBoards = Relation::query() ->where('user_id', get_current_user_id()) ->whereNotNull('board_id') ->where('status', 'ACTIVE') ->get(); foreach ($currentUserBoards as &$currentUserBoardPermission) { $currentUserBoardPermission->permissions = \maybe_unserialize($currentUserBoardPermission->permissions); } return $this->sendSuccess( $currentUserBoards, 200 ); } catch (\Exception $e) { return $this->sendError($e->getMessage(), 404); } } public function getUserPermission(Request $request) { try { $boardId = $request->getSafe('boardId', 'intval'); $userId = $request->getSafe('userId', 'intval'); $boardUser = Relation::where('board_id', $boardId) ->where('user_id', $userId) ->where('status', 'ACTIVE')->first(); if (!$boardUser->is_admin) { $boardUser->permissions = \maybe_unserialize($boardUser->permissions); } return $this->sendSuccess( $boardUser, 200 ); } catch (\Exception $e) { return $this->sendError($e->getMessage(), 404); } } public function updatedUserPermission(Request $request) { try { $permission = $request->getSafe('userPermission', 'sanitize_text_field'); $updateType = $request->getSafe('updateType', 'sanitize_text_field'); $boardId = $request->getSafe('boardId', 'intval'); $userId = $request->getSafe('userId', 'intval'); $boardUser = Relation::where('board_id', $boardId)->where('user_id', $userId)->status('ACTIVE')->first(); if ('Board Admin' == $permission) { $boardUser->is_admin = 'add' == $updateType ? 1 : 0; $boardUser->save(); } else { if (0 == $boardUser->is_admin) { $permissionsAlreadyHave = maybe_unserialize($boardUser->permissions); if (!in_array($permission, $permissionsAlreadyHave) && 'add' == $updateType) { array_push($permissionsAlreadyHave, $permission); } elseif (in_array($permission, $permissionsAlreadyHave) && 'remove' == $updateType) { if (($key = array_search($permission, $permissionsAlreadyHave)) !== false) { unset($permissionsAlreadyHave[$key]); } } $boardUser->permissions = serialize($permissionsAlreadyHave); $boardUser->save(); } } $boardUser->permissions = maybe_unserialize($boardUser->permissions); return $this->sendSuccess( $boardUser, 200 ); } catch (\Exception $e) { return $this->sendError($e->getMessage(), 404); } } public function SetUserSuperAdmin($userId) { $userId = absint($userId); try { $this->optionService->createSuperAdmin($userId); return $this->sendSuccess([ 'message' => __('Member has been set super admin successfully!', 'fluent-boards') ], 200); } catch (\Exception $e) { return $this->sendError($e->getMessage(), 404); } } public function removeUserSuperAdmin($userId) { $userId = absint($userId); try { $this->optionService->removeUserSuperAdmin($userId); return $this->sendSuccess([ 'message' => __('User has been removed as super admin successfully!', 'fluent-boards'), ], 200); } catch (\Exception $e) { return $this->sendError($e->getMessage(), 404); } } public function IsUserAllBoardAdmin(Request $request) { try { $userId = $request->getSafe('id', 'intval'); $isSuperAdmin = false; $superAdmin = Relation::where('board_id', null)->where('user_id', $userId)->where('status', 'ACTIVE')->first(); $totalSuperAdmin = Relation::where('board_id', null)->where('status', 'ACTIVE')->count(); $permissions = []; if ($superAdmin) { $isSuperAdmin = true; $permissions = \maybe_unserialize($superAdmin->permissions); } return $this->sendSuccess([ 'allBoardAdmin' => $isSuperAdmin, 'permissions' => $permissions, 'numberOfSuperAdmin' => $totalSuperAdmin, ], 200); } catch (\Exception $e) { return $this->sendError($e->getMessage(), 404); } } public function RemoveUserFromSuperAdmin(Request $request, $id) { $id = absint($id); try { $userId = $request->getSafe('id', 'intval'); $superAdmin = Relation::where('board_id', null)->where('user_id', $userId)->first(); $superAdmin->status = 'INACTIVE'; $superAdmin->save(); return $this->sendSuccess([ 'message' => __('User removed as super admin', 'fluent-boards'), ], 201); } catch (\Exception $e) { return $this->sendError($e->getMessage(), 404); } } public function removeUserFromBoard(Request $request) { try { $boardId = $request->getSafe('boardId', 'intval'); $userId = $request->getSafe('userId', 'intval'); $this->boardService->removeUserFromBoard($boardId, $userId); if (!PermissionManager::isAdmin($userId)) { $this->boardService->removeFromRecentlyOpened($boardId, $userId); } return $this->sendSuccess([ 'message' => __('User Removed from Board successfully!', 'fluent-boards'), ], 201); } catch (\Exception $e) { return $this->sendError($e->getMessage(), 404); } } public function addAsSuperAdmin(Request $request) { try { $rawUserIds = $request->getSafe('memberIds'); // Sanitize array of user IDs $userIds = []; if (is_array($rawUserIds)) { $userIds = array_filter(array_map('intval', $rawUserIds)); } foreach ($userIds as $userId) { $this->createSuperAdmin($userId); } return $this->sendSuccess([ 'message' => __('Fluent boards admin added', 'fluent-boards'), ], 201); } catch (\Exception $e) { return $this->sendError($e->getMessage(), 404); } } private function createSuperAdmin($userId) { try { $existUser = Meta::where('object_id', $userId)->first(); if (!$existUser) { $meta = new Meta(); $meta->object_id = $userId; $meta->object_type = Constant::FLUENT_BOARD_ADMIN; $meta->save(); } } catch (\Exception $e) { return $this->sendError($e->getMessage(), 404); } } public function addMembersInBoards(Request $request) { try { $rawUserIds = $request->getSafe('memberIds'); $rawBoardIds = $request->getSafe('boardIds'); // Sanitize arrays of IDs $userIds = []; if (is_array($rawUserIds)) { $userIds = array_filter(array_map('intval', $rawUserIds)); } $boardIds = []; if (is_array($rawBoardIds)) { $boardIds = array_filter(array_map('intval', $rawBoardIds)); } foreach ($userIds as $userId) { foreach ($boardIds as $boardId) { $this->boardService->addMembersInBoard($boardId, $userId); } } return $this->sendSuccess([ 'message' => __('Members added to boards', 'fluent-boards'), ], 201); } catch (\Exception $e) { return $this->sendError($e->getMessage(), 404); } } public function updateGlobalNotificationSettings(Request $request) { try { // updatedSettings is an array, sanitize each element $rawSettings = $request->get('updatedSettings'); $newSettings = []; if (is_array($rawSettings)) { foreach ($rawSettings as $key => $value) { $sanitizedKey = sanitize_text_field($key); $sanitizedValue = sanitize_text_field($value); $newSettings[$sanitizedKey] = $sanitizedValue; } } $this->optionService->updateGlobalNotificationSettings($newSettings); return $this->sendSuccess([ 'message' => __("Notification settings are updated", 'fluent-boards'), ], 201); } catch (\Exception $e) { return $this->sendError($e->getMessage(), 404); } } public function getGlobalNotificationSettings() { try { $globalSettings = $this->optionService->getGlobalNotificationSettings(); if ($globalSettings->value) $currentSettings = maybe_unserialize($globalSettings->value); return $this->sendSuccess([ 'currentSettings' => $currentSettings, ], 200); } catch (\Exception $e) { return $this->sendError($e->getMessage(), 404); } } public function getBoardMembers(Request $request) { if (!PermissionManager::userHasAnyBoardAccess()) { return $this->sendError([ 'message' => __('You do not have permission to access this route', 'fluent-boards') ]); } $boardId = $request->getSafe('boardId', 'intval'); if ($boardId && !PermissionManager::userHasPermission($boardId)) { return $this->sendError([ 'message' => __('You do not have permission to access this route', 'fluent-boards') ], 403); } $memberUserIdsQuery = Relation::where('object_type', Constant::OBJECT_TYPE_BOARD_USER) ->select(['foreign_id']) ->groupBy('foreign_id'); if ($boardId) { $memberUserIdsQuery->where('object_id', $boardId); } elseif (!PermissionManager::isAdmin()) { $boardIds = array_filter(array_map('intval', PermissionManager::getBoardIdsForUser())); $memberUserIdsQuery->whereIn('object_id', $boardIds); } $memberUserIds = $memberUserIdsQuery->get() ->pluck('foreign_id')->toArray(); $members = []; if ($memberUserIds) { $memberUsers = get_users([ 'include' => $memberUserIds ]); foreach ($memberUsers as $memberUser) { $name = trim($memberUser->first_name . ' ' . $memberUser->last_name); if (!$name) { $name = $memberUser->display_name; } $members[$memberUser->ID] = [ 'ID' => $memberUser->ID, 'display_name' => $name, 'photo' => get_avatar_url($memberUser->user_email) ]; } } $adminUsers = get_users([ 'role' => 'administrator', 'exclude' => $memberUserIds ]); foreach ($adminUsers as $user) { $name = trim($user->first_name . ' ' . $user->last_name); if (!$name) { $name = $user->display_name; } $members[$user->ID] = [ 'ID' => $user->ID, 'display_name' => $name, 'photo' => get_avatar_url($user->user_email) ]; } $members = array_values($members); // sort members by name usort($members, function ($a, $b) { return strcmp($a['display_name'], $b['display_name']); }); return [ 'members' => $members ]; } public function globalSearch() { $currentUserId = get_current_user_id(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API $query = isset($_REQUEST['query']) ? strtolower(sanitize_text_field(wp_unslash($_REQUEST['query']))) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API $scope = isset($_REQUEST['scope']) ? sanitize_text_field(wp_unslash($_REQUEST['scope'])) : 'all'; // Pagination parameters // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API $taskPage = isset($_REQUEST['task_page']) ? max(1, (int)$_REQUEST['task_page']) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API $boardPage = isset($_REQUEST['board_page']) ? max(1, (int)$_REQUEST['board_page']) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, nonce verification handled by WordPress REST API $perPage = isset($_REQUEST['per_page']) ? (int)$_REQUEST['per_page'] : 20; $perPage = max(1, min(100, $perPage)); // Build base queries $firstThreeChars = substr($query, 0, 3); $firstNineChars = substr($query, 0, 9); if($firstThreeChars == 'id:') { $idPart = preg_replace('/[^a-zA-Z0-9]/', '', substr($query, 3)); $tasksQuery = Task::query()->where('parent_id', null)->whereRaw('id LIKE ?', ['%' . $idPart . '%']); $boardQuery = Board::query()->whereRaw('id LIKE ?', ['%' . $idPart . '%']); }elseif($firstNineChars == 'archived:') { $archivedPart = trim(substr($query, 9)); $tasksQuery = Task::query()->where('parent_id', null)->whereNotNull('archived_at')->whereRaw('LOWER(title) LIKE ?', ['%' . $archivedPart . '%']); $boardQuery = Board::query()->whereNotNull('archived_at')->whereRaw('LOWER(title) LIKE ?', ['%' . $archivedPart . '%']); } else { $tasksQuery = Task::query()->where('parent_id', null)->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']); $boardQuery = Board::query()->whereRaw('LOWER(title) LIKE ?', ['%' . $query . '%']); } // Apply scope and permissions $isUserAdmin = PermissionManager::isAdmin($currentUserId); if ($scope == 'all') { if (!$isUserAdmin) { $boardIds = PermissionManager::getBoardIdsForUser($currentUserId); $boardQuery->whereIn('id', $boardIds); $tasksQuery->whereIn('board_id', $boardIds); } } else { // For 'current_board' scope, we don't search boards $boardQuery->where('id', -1); $inBoard = absint($scope); if ($isUserAdmin || in_array($inBoard, PermissionManager::getBoardIdsForUser($currentUserId))) { $tasksQuery->where('board_id', $inBoard); } else { $tasksQuery->where('id', -1); // Force no results } } $allActiveBoardsIds = Board::query()->where('archived_at', null)->pluck('id')->toArray(); $boards = []; $tasks = []; $totalBoards = 0; $totalTasks = 0; $formattedBoards = []; $formattedTasks = []; // Fetch Boards if requested if ($boardPage > 0) { $totalBoards = $boardQuery->count(); $boardOffset = ($boardPage - 1) * $perPage; $boards = $boardQuery->skip($boardOffset)->take($perPage)->get(); } // Fetch Tasks if requested if ($taskPage > 0) { $totalTasks = $tasksQuery->count(); $taskOffset = ($taskPage - 1) * $perPage; $tasks = $tasksQuery->skip($taskOffset)->take($perPage)->get(); } foreach ($boards as $board) { $formattedBoards[] = [ 'type' => 'board', 'id' => $board->id, 'title' => $board->title, 'description' => DescriptionMarkdownConverter::normalize($board->description), ]; } foreach ($tasks as $task) { if (!in_array($task->board_id, $allActiveBoardsIds)) { continue; } $board = $task->board; $formattedTasks[] = [ 'type' => 'task', 'id' => $task->id, 'title' => $task->title, 'description' => DescriptionMarkdownConverter::normalize($task->description), 'board_id' => $task->board_id, 'board' => [ 'id' => $board->id, 'title' => $board->title, 'url' => Helper::getBoardUrl($board->id) ], 'stage' => [ 'id' => $task->stage_id, 'title' => $task->stage->title ?? '', ], ]; } return $this->sendSuccess([ 'tasks' => [ 'data' => $formattedTasks, 'current_page' => $taskPage, 'per_page' => $perPage, 'total' => $totalTasks, 'last_page' => (int) ceil($totalTasks / $perPage) ], 'boards' => [ 'data' => $formattedBoards, 'current_page' => $boardPage, 'per_page' => $perPage, 'total' => $totalBoards, 'last_page' => (int) ceil($totalBoards / $perPage) ] ], 200); } public function getDashboardViewSettings(Request $request) { $view = $request->getSafe('view', 'sanitize_text_field'); if ($view == 'kanbanview') { $globalSettings = $this->optionService->getDashboardViewSettings(); } elseif ($view == 'listview') { $globalSettings = $this->optionService->getListViewPreferences(); } elseif ($view == 'tableview') { $globalSettings = $this->optionService->getTableViewPreferences(); } else { // Handle invalid view or default to one return $this->sendError(['message' => __('Invalid view type', 'fluent-boards')], 400); } if ($globalSettings->value) $currentSettings = maybe_unserialize($globalSettings->value); return $this->sendSuccess([ 'currentSettings' => $currentSettings, ], 200); } public function updateDashboardViewSettings(Request $request) { // updatedSettings is an array, sanitize each element $rawSettings = $request->get('updatedSettings'); $newSettings = []; if (is_array($rawSettings)) { foreach ($rawSettings as $key => $value) { $sanitizedKey = sanitize_text_field($key); // Value could be string, boolean, or number - sanitize appropriately if (is_string($value)) { $sanitizedValue = sanitize_text_field($value); } elseif (is_bool($value) || is_numeric($value)) { $sanitizedValue = $value; } else { $sanitizedValue = sanitize_text_field((string)$value); } $newSettings[$sanitizedKey] = $sanitizedValue; } } $view = $request->getSafe('view', 'sanitize_text_field'); $this->optionService->updateDashboardViewSettings($newSettings, $view); if ($view == 'listview') { $message = __("List view settings updated successfully", 'fluent-boards'); } elseif ($view == 'tableview') { $message = __("Table view settings updated successfully", 'fluent-boards'); } else { $message = __("Card view settings updated successfully", 'fluent-boards'); } return $this->sendSuccess([ 'message' => $message, ], 201); } public function getAddonsSettings() { $canAutoInstallKit = $this->canAutoInstallFluentKit(); $kitPluginFile = 'fluent-toolkit/fluent-toolkit.php'; $kitLoaded = defined('FLUENT_TOOLKIT_VERSION'); $kitPluginExists = $this->isPluginInstalled($kitPluginFile); $kitActionText = __('Get FluentHub from GitHub', 'fluent-boards'); if ($canAutoInstallKit) { $kitActionText = $kitPluginExists ? __('Activate FluentHub', 'fluent-boards') : __('Install FluentHub', 'fluent-boards'); } $addOns = [ 'fluent-crm' => [ 'title' => __('FluentCRM', 'fluent-boards'), 'logo' => fluent_boards_mix('images/addons/fluent-crm.svg'), 'is_installed' => defined('FLUENTCRM'), 'learn_more_url' => 'https://fluentcrm.com/', 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluentcrm/', 'action_text' => $this->isPluginInstalled('fluent-crm/fluent-crm.php') ? __('Activate FluentCRM', 'fluent-boards') : __('Install FluentCRM', 'fluent-boards'), 'description' => __('FluentCRM is a Self Hosted Email Marketing Automation Plugin for WordPress. Manage your leads and customers, email campaigns, automated email sequencing and many more', 'fluent-boards'), 'short_desc' => __('Email marketing automation', 'fluent-boards') ], 'fluentform' => [ 'title' => __('Fluent Forms', 'fluent-boards'), 'logo' => fluent_boards_mix('images/addons/fluentform.png'), 'is_installed' => defined('FLUENTFORM'), 'learn_more_url' => 'https://wordpress.org/plugins/fluentform/', 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluent-forms/', 'action_text' => $this->isPluginInstalled('fluent-form/fluent-form.php') ? __('Activate Fluent Forms', 'fluent-boards') : __('Install Fluent Forms', 'fluent-boards'), 'description' => __('Collect leads and build any type of forms, accept payments, connect with your CRM with the Fastest Contact Form Builder Plugin for WordPress', 'fluent-boards'), 'short_desc' => __('Create forms and accept payments', 'fluent-boards') ], 'fluent-support' => [ 'title' => __('Fluent Support', 'fluent-boards'), 'logo' => fluent_boards_mix('images/addons/fluent-support.svg'), 'is_installed' => defined('FLUENT_SUPPORT_VERSION'), 'learn_more_url' => 'https://wordpress.org/plugins/fluent-connect/', 'settings_url' => admin_url('admin.php?page=fluent-support#/'), 'associate_doc' => 'https://fluentboards.com/docs/fluentboards-integration-with-fluentsupport/', 'action_text' => $this->isPluginInstalled('fluent-support/fluent-support.php') ? __('Activate Fluent Support', 'fluent-boards') : __('Install Fluent Support', 'fluent-boards'), 'description' => __('WordPress Helpdesk and Customer Support Ticket Plugin. Provide awesome support and manage customer queries right from your WordPress dashboard.', 'fluent-boards'), 'short_desc' => __('Customer support ticketing', 'fluent-boards') ], 'fluent-smtp' => [ 'title' => __('Fluent SMTP', 'fluent-boards'), 'logo' => fluent_boards_mix('images/addons/fluent-smtp.svg'), 'is_installed' => defined('FLUENTMAIL'), 'learn_more_url' => 'https://wordpress.org/plugins/fluent-smtp/', 'associate_doc' => admin_url('options-general.php?page=fluent-mail#/'), 'action_text' => $this->isPluginInstalled('fluent-smtp/fluent-smtp.php') ? __('Activate Fluent SMTP', 'fluent-boards') : __('Install Fluent SMTP', 'fluent-boards'), 'description' => __('The Ultimate SMTP and SES Plugin for WordPress. Connect with any SMTP, SendGrid, Mailgun, SES, Sendinblue, PepiPost, Google, Microsoft and more.', 'fluent-boards'), 'short_desc' => __('Reliable email delivery with SMTP', 'fluent-boards') ], 'fluent-toolkit' => [ 'title' => __('FluentHub', 'fluent-boards'), 'logo' => fluent_boards_mix('images/addons/fluent-toolkit.svg'), 'is_installed' => $kitLoaded, 'learn_more_url' => 'https://github.com/WPManageNinja/fluent-toolkit', 'settings_url' => admin_url('admin.php?page=fluent-toolkit'), 'associate_doc' => 'https://github.com/WPManageNinja/fluent-toolkit', 'action_text' => $kitActionText, 'install_route' => $canAutoInstallKit ? 'admin/mcp/install-adapter' : '', 'install_url' => $canAutoInstallKit ? '' : 'https://github.com/WPManageNinja/fluent-toolkit', 'description' => __('Fluent Boards MCP tools become available after FluentHub is installed and active.', 'fluent-boards'), 'short_desc' => __('AI agent tools for Fluent Boards', 'fluent-boards') ], ]; $addOns = apply_filters('fluent_boards/addons_settings', $addOns); $modules = fluent_boards_get_pref_settings(false); if (empty($modules['frontend']['render_type'])) { $modules['frontend']['render_type'] = 'standalone'; } $modules['panel_url'] = fluent_boards_page_url(); return [ 'addons' => $addOns, 'featureModules' => $modules ]; } public function saveAddonsSettings(Request $request) { if (!defined('FLUENT_BOARDS_PRO')) { return $this->sendError([ 'message' => __('This feature is only available in Fluent Boards Pro', 'fluent-boards') ]); } $rawSettings = $request->get('settings', []); // Validate that settings is an array if (!is_array($rawSettings)) { return $this->sendError([ 'message' => __('Invalid settings format', 'fluent-boards') ], 400); } $prefSettings = fluent_boards_get_pref_settings(false); $settings = wp_parse_args($rawSettings, $prefSettings); $settings = Arr::only($settings, array_keys($prefSettings)); // Sanitize slug if it exists if (isset($settings['frontend']['slug'])) { $settings['frontend']['slug'] = sanitize_title($settings['frontend']['slug']); } if (empty($settings['frontend']['slug'])) { $settings['frontend']['slug'] = 'projects'; } if (defined('FLUENT_BOARDS_SLUG') && FLUENT_BOARDS_SLUG) { $settings['frontend']['slug'] = FLUENT_BOARDS_SLUG; } do_action('fluent_boards/saving_addons', $settings, $prefSettings); update_option('fluent_boards_modules', $settings, 'yes'); if (isset($settings['recurring_task']['enabled']) && $settings['recurring_task']['enabled'] == 'no') { do_action('fluent_boards/recurring_task_disabled'); } return $this->sendSuccess([ 'message' => __('Settings are saved', 'fluent-boards'), 'featureModules' => $settings ]); } public function installPlugin(Request $request) { if (!current_user_can('install_plugins')) { return $this->sendError([ 'message' => __('Sorry! you do not have permission to install plugin', 'fluent-boards') ]); } $plugin = $request->getSafe('plugin', 'sanitize_text_field'); $acceptedFreePlugins = [ 'fluent-crm' => 'fluent-crm.php', 'fluentform' => 'fluentform.php', 'fluent-support' => 'fluent-support.php', 'fluent-smtp' => 'fluent-smtp.php' ]; $acceptedPlugins = apply_filters('fluent_boards/accepted_plugins', $acceptedFreePlugins); if (!isset($acceptedPlugins[$plugin])) { return $this->sendError([ 'message' => __('Invalid plugin', 'fluent-boards') ]); } $pluginToInstall = [ 'name' => __('Fluent Plugin', 'fluent-boards'), 'repo-slug' => $plugin, 'file' => $acceptedPlugins[$plugin], ]; // if plugin in free list then run background intaller otherwise call an action to install if (isset($acceptedFreePlugins[$plugin])) { $this->backgroundInstaller($pluginToInstall, $plugin); } else { do_action('fluent_boards/install_plugin', $pluginToInstall, $plugin); } return $this->sendSuccess([ 'message' => __('Plugin is being installed', 'fluent-boards') ]); } private function isPluginInstalled($plugin) { return file_exists(WP_PLUGIN_DIR . '/' . $plugin); } private function canAutoInstallFluentKit() { $canAutoInstall = (bool) apply_filters('fluent_kit/can_auto_install', false); if (!$canAutoInstall) { $canAutoInstall = (bool) apply_filters('fluent_toolkit/can_auto_install', false); } return $canAutoInstall; } private function backgroundInstaller($plugin_to_install, $plugin_id) { if (!empty($plugin_to_install['repo-slug'])) { require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; require_once ABSPATH . 'wp-admin/includes/plugin.php'; WP_Filesystem(); $skin = new \Automatic_Upgrader_Skin(); $upgrader = new \WP_Upgrader($skin); $installed_plugins = array_reduce(array_keys(\get_plugins()), array($this, 'associate_plugin_file'), array()); $plugin_slug = $plugin_to_install['repo-slug']; $plugin_file = isset($plugin_to_install['file']) ? $plugin_to_install['file'] : $plugin_slug . '.php'; $installed = false; $activate = false; // See if the plugin is installed already. if (isset($installed_plugins[$plugin_file])) { $installed = true; $activate = !is_plugin_active($installed_plugins[$plugin_file]); } // Install this thing! if (!$installed) { // Suppress feedback. ob_start(); try { $plugin_information = plugins_api( 'plugin_information', array( 'slug' => $plugin_slug, 'fields' => array( 'short_description' => false, 'sections' => false, 'requires' => false, 'rating' => false, 'ratings' => false, 'downloaded' => false, 'last_updated' => false, 'added' => false, 'tags' => false, 'homepage' => false, 'donate_link' => false, 'author_profile' => false, 'author' => false, ), ) ); if (is_wp_error($plugin_information)) { throw new \Exception(esc_html($plugin_information->get_error_message())); } $package = $plugin_information->download_link; $download = $upgrader->download_package($package); if (is_wp_error($download)) { throw new \Exception(esc_html($download->get_error_message())); } $working_dir = $upgrader->unpack_package($download, true); if (is_wp_error($working_dir)) { throw new \Exception(esc_html($working_dir->get_error_message())); } $result = $upgrader->install_package( array( 'source' => $working_dir, 'destination' => WP_PLUGIN_DIR, 'clear_destination' => false, 'abort_if_destination_exists' => false, 'clear_working' => true, 'hook_extra' => array( 'type' => 'plugin', 'action' => 'install', ), ) ); if (is_wp_error($result)) { throw new \Exception(esc_html($result->get_error_message())); } $activate = true; } catch (\Exception $e) { } // Discard feedback. ob_end_clean(); } wp_clean_plugins_cache(); // Activate this thing. if ($activate) { try { $result = activate_plugin($installed ? $installed_plugins[$plugin_file] : $plugin_slug . '/' . $plugin_file); if (is_wp_error($result)) { throw new \Exception(esc_html($result->get_error_message())); } } catch (\Exception $e) { } } } } private function associate_plugin_file($plugins, $key) { $path = explode('/', $key); $filename = end($path); $plugins[$filename] = $key; return $plugins; } public function getBoards(Request $request) { $boards = Board::select(['id', 'title', 'type']) ->byAccessUser(get_current_user_id()) ->orderBy('title', 'ASC') ->get(); return $this->sendSuccess([ 'boards' => $boards ]); } public function getPages(Request $request) { $db = App::getInstance('db'); $allPages = $db->table('posts')->where('post_type', 'page') ->where('post_status', 'publish') ->select(['ID', 'post_title']) ->orderBy('post_title', 'ASC') ->get(); $pages = []; foreach ($allPages as $page) { $pages[] = [ 'id' => $page->ID, 'title' => $page->post_title ? $page->post_title : __('(no title)', 'fluent-boards'), 'url' => esc_url_raw(get_permalink($page->ID)) ]; } return $this->sendSuccess([ 'pages' => $pages ]); } public function getGeneralSettings() { $settings = fluent_boards_get_option('general_settings', []); return $this->sendSuccess([ 'settings' => $settings, 'server_timezone' => \wp_timezone_string() ]); } public function saveGeneralSettings(Request $request) { // check for pro version if (!defined('FLUENT_BOARDS_PRO')) { return $this->sendError([ 'message' => __('This feature is only available in Fluent Boards Pro. Please upgrade.', 'fluent-boards') ]); } // updatedSettings is an array, sanitize each element $rawSettings = $request->get('updatedSettings', []); $settings = []; if (is_array($rawSettings)) { foreach ($rawSettings as $key => $value) { $sanitizedKey = sanitize_text_field($key); // Value could be string, boolean, or number - sanitize appropriately if (is_string($value)) { $sanitizedValue = sanitize_text_field($value); } elseif (is_bool($value) || is_numeric($value)) { $sanitizedValue = $value; } else { $sanitizedValue = sanitize_text_field((string)$value); } $settings[$sanitizedKey] = $sanitizedValue; } } $settings = apply_filters('fluent_boards/save_general_settings', $settings); $savedSettings = fluent_boards_update_option('general_settings', $settings); $savedGeneralSettings = \maybe_unserialize($savedSettings->value); $scheduleHandler = new ProScheduleHandler(); $dailyReminderEnabled = $savedGeneralSettings['daily_reminder_enabled'] ?? false; if (filter_var($dailyReminderEnabled, FILTER_VALIDATE_BOOLEAN)) { // force schedule from this settings update $scheduleHandler->clearDailyTaskReminderScheduler(); $scheduleHandler->scheduleDailyTaskReminder(); } return $this->sendSuccess([ 'settings' => $savedGeneralSettings, 'message' => __('Settings are saved', 'fluent-boards') ]); } }