firstOrFail(); if ($xprofile->status != 'active' && !Helper::isModerator()) { return $this->sendError([ 'message' => __('This profile is not active', 'fluent-community') ], 403); } $canViewProfile = Utility::canViewUserProfile($xprofile->user_id); $user = get_user_by('ID', $xprofile->user_id); $profile = [ 'user_id' => $xprofile->user_id, 'is_verified' => $xprofile->is_verified, 'display_name' => $xprofile->display_name, 'username' => $xprofile->username, 'avatar' => $xprofile->avatar, 'has_custom_avatar' => $xprofile->hasCustomAvatar(), 'cover_photo' => Arr::get($xprofile->meta, 'cover_photo'), 'headline' => Arr::get($xprofile->meta, 'headline', ''), 'total_points' => $xprofile->total_points, 'badge_slugs' => (array)Arr::get($xprofile->meta, 'badge_slug', []), 'status' => $xprofile->status, 'is_restricted' => !$canViewProfile, 'canViewUserSpaces' => ProfileHelper::canViewUserSpaces($xprofile->user_id, $this->getUser()) ]; if (Utility::showLastActivity()) { $profile['last_activity'] = $xprofile->last_activity; } if ($canViewProfile) { $profile['website'] = Arr::get($xprofile->meta, 'website'); $profile['created_at'] = $xprofile->created_at->format('Y-m-d H:i:s'); $profile['social_links'] = (object) Arr::get($xprofile->meta, 'social_links', []); $profile['compilation_score'] = $xprofile->getCompletionScore(); $profile['short_description_rendered'] = wp_kses_post(FeedsHelper::mdToHtml($xprofile->short_description)); } $currentUserId = get_current_user_id(); $isOwn = $xprofile->user_id == $currentUserId; $isAdmin = Helper::isSiteAdmin($currentUserId); if ($isOwn || $isAdmin) { $enableUserSync = Utility::getPrivacySetting('enable_user_sync') === 'yes'; $nameArray = explode(' ', trim((string) $xprofile->display_name)); $xprofileFirstName = array_shift($nameArray); $xprofileLastName = implode(' ', $nameArray); $profile['email'] = $user->user_email; $profile['first_name'] = $enableUserSync ? $user->first_name : $xprofileFirstName; $profile['last_name'] = $enableUserSync ? $user->last_name : $xprofileLastName; $profile['short_description'] = $xprofile->short_description; $profile['can_change_username'] = $isAdmin || Utility::getPrivacySetting('can_customize_username') === 'yes'; $profile['can_change_email'] = current_user_can('edit_users') || (Utility::getPrivacySetting('can_change_email') === 'yes' && $isOwn); $profile['can_change_password'] = $isOwn && Utility::getPrivacySetting('can_change_password') === 'yes'; } $profileBaseUrl = Helper::baseUrl('u/' . $xprofile->username . '/'); $profile['profile_navs'] = [ [ 'slug' => 'user_profile', 'title' => __('About', 'fluent-community'), 'url' => $profileBaseUrl, 'wrapper_class' => 'fcom_profile_about', 'route' => [ 'name' => 'user_profile' ] ], [ 'slug' => 'user_profile_feeds', 'title' => __('Posts', 'fluent-community'), 'wrapper_class' => 'fcom_profile_posts', 'url' => $profileBaseUrl . 'posts', 'route' => [ 'name' => 'user_profile_feeds' ] ] ]; if ($profile['canViewUserSpaces']) { $profile['profile_navs'][] = [ 'slug' => 'user_spaces', 'wrapper_class' => 'fcom_profile_spaces', 'title' => __('Spaces', 'fluent-community'), 'url' => $profileBaseUrl . 'spaces', 'route' => [ 'name' => 'user_spaces' ] ]; if (Helper::isFeatureEnabled('course_module')) { $profile['profile_navs'][] = [ 'slug' => 'user_courses', 'wrapper_class' => 'fcom_profile_courses', 'title' => __('Courses', 'fluent-community'), 'url' => $profileBaseUrl . 'courses', 'route' => [ 'name' => 'user_courses' ] ]; } } $profile['profile_navs'][] = [ 'slug' => 'user_comments', 'wrapper_class' => 'fcom_profile_comments', 'title' => __('Comments', 'fluent-community'), 'url' => $profileBaseUrl . 'comments', 'route' => [ 'name' => 'user_comments' ] ]; $profile['profile_nav_actions'] = []; $profile = apply_filters('fluent_community/profile_view_data', $profile, $xprofile, $isAdmin); return [ 'profile' => $profile ]; } public function patchProfile(Request $request, $userName) { $xprofile = $this->verifyAndGetProfile($userName); $updateData = $request->get('data', []); if (!empty($updateData['status']) && $updateData['status'] === 'deactivated' && $xprofile->status === 'active') { // handle deactivation $canDeactivate = Utility::getPrivacySetting('can_deactive_account') === 'yes' || Helper::isSiteAdmin(); if (!$canDeactivate) { return $this->sendError([ 'message' => __('You are not allowed to deactivate this account.', 'fluent-community') ]); } $xprofile->status = ''; $xprofile->save(); update_user_meta($xprofile->user_id, '_fcom_deactivated_at', current_time('mysql')); do_action('fluent_community/profile_deactivated', $xprofile); return [ 'message' => __('Your profile has been deactivated successfully.', 'fluent-community') ]; } $mediaTypes = ['cover_photo', 'avatar']; foreach ($mediaTypes as $type) { if (!empty($updateData[$type])) { $media = Helper::getMediaFromUrl($updateData[$type]); if (!$media || $media->is_active) { return $this->sendError([ 'message' => __('Invalid media image. Please upload a new one.', 'fluent-community') ]); } $updateData[$type] = $media->public_url; $media->update([ 'is_active' => true, 'user_id' => $xprofile->user_id, 'object_source' => 'user_' . $type ]); } } $deletedMedias = []; if (isset($updateData['avatar'])) { if ($xprofile->hasCustomAvatar()) { $deletedMedias[] = Arr::get($xprofile->getAttributes(), 'avatar'); } $xprofile->avatar = $updateData['avatar']; if (defined('FLUENTCRM')) { $contact = $xprofile->contact; if ($contact) { $contact->update([ 'avatar' => $updateData['avatar'] ?: null ]); } } if (empty($updateData['avatar'])) { Utility::forgetCache('user_avatar_' . $xprofile->user_id); } } if (isset($updateData['cover_photo'])) { $deletedMedias[] = Arr::get($xprofile->meta, 'cover_photo'); $xprofile->meta = wp_parse_args(['cover_photo' => $updateData['cover_photo']], $xprofile->meta); } $xprofile->save(); if ($deletedMedias = array_filter($deletedMedias)) { do_action('fluent_community/remove_medias_by_url', $deletedMedias, [ 'user_id' => $xprofile->user_id, 'object_sources' => ['user_avatar', 'user_cover_photo'] ]); } return [ 'message' => __('Profile updated', 'fluent-community') ]; } public function updateProfile(Request $request, $userName) { $currentUser = $this->getUser(true); $data = $request->get('data', []); /** @var XProfile $xProfile */ $xProfile = XProfile::where('username', $userName)->firstOrFail(); if ($xProfile->user_id != get_current_user_id()) { if(!$currentUser->isCommunityModerator()) { return $this->sendError([ 'message' => __('You are not allowed to update this profile', 'fluent-community') ]); } } $this->validate($data, [ 'first_name' => 'required', ], [ 'first_name.required' => __('First name is required', 'fluent-community') ]); $updateData = Arr::only($data, ['first_name', 'last_name', 'short_description', 'website']); $updateData = apply_filters('fluent_community/update_profile_data', $updateData, $data, $xProfile, $currentUser); $meta = $xProfile->meta; $userNameChanged = false; if ($currentUser->isCommunityModerator()) { $updateData['is_verified'] = Arr::get($data, 'is_verified') ? 1 : 0; $updateData['status'] = Arr::get($data, 'status', 'active'); $userName = Arr::get($data, 'username'); if (user_can($xProfile->user_id, 'list_users')) { $updateData['status'] = 'active'; } if ($userName) { // Check if username is exit or not $userName = CustomSanitizer::sanitizeUserName($userName); if (!$userName) { return $this->sendError([ 'message' => __('Invalid username. Only Latin characters with _ & - are allowed.', 'fluent-community') ]); } if (XProfile::where('username', $userName)->where('user_id', '!=', $xProfile->user_id)->exists()) { return $this->sendError([ 'message' => __('Community Username already taken by someone else', 'fluent-community') ]); } $userExist = get_user_by('user_login', $userName); if ($userExist && $userExist->ID != $xProfile->user_id) { return $this->sendError([ 'message' => __('Username already taken by someone else. Please use a different username.', 'fluent-community') ]); } $updateData['username'] = $userName; $userNameChanged = $userName != $xProfile->username; } if (Helper::isFeatureEnabled('user_badge')) { $badgeSlug = array_filter((array) Arr::get($data, 'badge_slugs', []), 'is_scalar'); $badgeSlug = array_map('sanitize_text_field', $badgeSlug); $definedBadges = (array) Utility::getOption('user_badges', []); $meta['badge_slug'] = array_values(array_intersect($badgeSlug, array_keys($definedBadges))); } } else if (Utility::getPrivacySetting('can_customize_username')) { $userName = Arr::get($data, 'username'); if ($xProfile->username != $userName) { $userName = strtolower(CustomSanitizer::sanitizeUserName($userName)); if (!$userName) { return $this->sendError([ 'message' => __('Invalid username. Only Latin characters with _ & - are allowed.', 'fluent-community') ]); } if (XProfile::where('username', $userName)->where('user_id', '!=', $xProfile->user_id)->exists()) { return $this->sendError([ 'message' => __('Community Username already taken by someone else', 'fluent-community') ]); } if (strlen($userName) < 3) { return $this->sendError([ 'message' => __('Username should be at least 3 characters long.', 'fluent-community') ]); } $reservedUserNames = ProfileHelper::getReservedUserNames(); if (in_array($userName, $reservedUserNames)) { return $this->sendError([ 'message' => __('Please use another username. This username is reserved', 'fluent-community') ]); } $updateData['username'] = $userName; $userNameChanged = true; } } $updateData['display_name'] = trim(sanitize_text_field(Arr::get($data, 'first_name') . ' ' . Arr::get($data, 'last_name'))); $updateData['short_description'] = CustomSanitizer::unslashMarkdown(sanitize_textarea_field(trim((string) Arr::get($data, 'short_description', '')))); $meta['website'] = sanitize_url((string) Arr::get($data, 'website', '')); $meta['headline'] = sanitize_text_field(trim(Arr::get($data, 'headline', ''))); $socialLinks = Arr::get($data, 'social_links', []); $maxDescriptionLength = apply_filters('fluent_community/max_profile_description_length', 5000); if ($updateData['short_description'] && strlen($updateData['short_description']) > $maxDescriptionLength) { return $this->sendError([ 'message' => sprintf( /* translators: %d: Maximum number of characters allowed in the profile bio. */ __('Profile bio should not exceed %d characters.', 'fluent-community'), $maxDescriptionLength ) ]); } $maxHeadlineLength = apply_filters('fluent_community/max_profile_headline_length', 60); if ($meta['headline'] && mb_strlen($meta['headline']) > $maxHeadlineLength) { return $this->sendError([ 'message' => sprintf( /* translators: %d: Maximum number of characters allowed in the profile headline. */ __('Headline should not exceed %d characters.', 'fluent-community'), $maxHeadlineLength ) ]); } if ($socialLinks) { $socialLinks = array_filter($socialLinks); $formattedSocialLinkes = []; $socialLinkProviders = ProfileHelper::socialLinkProviders(true); foreach ($socialLinks as $linkName => $socialLink) { if (isset($socialLinkProviders[$linkName])) { $formattedSocialLinkes[$linkName] = sanitize_text_field(trim($socialLink)); } } $meta['social_links'] = $formattedSocialLinkes; } $meta['short_description_rendered'] = wp_kses_post(FeedsHelper::mdToHtml($updateData['short_description'])); $updateData['meta'] = $meta; $xProfile->fill($updateData); $xProfile->save(); // Let's update the user's details $xProfile->user->updateCustomData($updateData); $xProfile->compilation_score = $xProfile->getCompletionScore(); if ($userNameChanged) { return [ 'message' => __('Profile has been updated', 'fluent-community'), 'profile' => $xProfile, 'redirect_url' => Helper::baseUrl('u/' . $xProfile->username . '/update') ]; } $isOwn = $xProfile->user_id == get_current_user_id(); $canEditUsers = current_user_can('edit_users'); if ($canEditUsers || (Utility::getPrivacySetting('can_change_email') === 'yes' && $isOwn)) { $emailAddress = Arr::get($data, 'email'); if ($emailAddress && is_email($emailAddress) && $emailAddress != $xProfile->user->user_email) { $owner_id = email_exists($emailAddress); if ($owner_id && $owner_id != $xProfile->user_id) { return $this->sendError([ 'message' => __('Email address already taken by someone else. Please use a different email address.', 'fluent-community') ]); } // Let's check if it's their own $requireVerification = $isOwn && !$canEditUsers; if ($requireVerification) { $currentUser = get_user_by('ID', $xProfile->user_id); ProfileHelper::sendConfirmationOnProfileEmailChange($currentUser, $emailAddress); return [ 'message' => __('Email address change is pending. Please check your inbox to verify the new email address.', 'fluent-community'), 'profile' => $xProfile ]; } wp_update_user([ 'user_email' => $emailAddress, 'ID' => $xProfile->user_id ]); } } return [ 'message' => __('Profile has been updated', 'fluent-community'), 'profile' => $xProfile ]; } public function changePassword(Request $request, $userName) { $xProfile = XProfile::where('username', $userName)->firstOrFail(); // Password can only be changed by the account owner, never by moderators/admins here. if ($xProfile->user_id != get_current_user_id()) { return $this->sendError([ 'message' => __('You are not allowed to change this password', 'fluent-community') ]); } if (Utility::getPrivacySetting('can_change_password') !== 'yes') { return $this->sendError([ 'message' => __('Password change is disabled', 'fluent-community') ]); } $data = $request->get('data', []); $this->validate($data, [ 'current_password' => 'required', 'new_password' => 'required', 'confirm_password' => 'required', ], [ 'current_password.required' => __('Current password is required', 'fluent-community'), 'new_password.required' => __('New password is required', 'fluent-community'), 'confirm_password.required' => __('Please confirm your new password', 'fluent-community'), ]); // Passwords are used verbatim; sanitizing would corrupt valid characters. $currentPassword = (string) Arr::get($data, 'current_password'); $newPassword = (string) Arr::get($data, 'new_password'); $confirmPassword = (string) Arr::get($data, 'confirm_password'); if (strlen($newPassword) < 4) { return $this->sendError([ 'message' => __('New password must be at least 4 characters long', 'fluent-community') ]); } if ($newPassword !== $confirmPassword) { return $this->sendError([ 'message' => __('New password and confirmation do not match', 'fluent-community') ]); } if ($newPassword === $currentPassword) { return $this->sendError([ 'message' => __('New password must be different from your current password', 'fluent-community') ]); } $user = get_user_by('id', $xProfile->user_id); if (!$user || !wp_check_password($currentPassword, $user->user_pass, $user->ID)) { return $this->sendError([ 'message' => __('Your current password is incorrect', 'fluent-community') ]); } wp_set_password($newPassword, $user->ID); // wp_set_password destroys every session for the user, which also invalidates the // REST nonce the SPA holds. Re-issue the cookie to keep the session, capturing the // fresh logged-in cookie so the nonces we mint below bind to the new session token. $newLoggedInCookie = ''; $captureLoggedInCookie = function ($loggedInCookie) use (&$newLoggedInCookie) { $newLoggedInCookie = $loggedInCookie; }; add_action('set_logged_in_cookie', $captureLoggedInCookie); wp_set_current_user($user->ID); wp_set_auth_cookie($user->ID, true); remove_action('set_logged_in_cookie', $captureLoggedInCookie); if ($newLoggedInCookie) { $_COOKIE[LOGGED_IN_COOKIE] = $newLoggedInCookie; } do_action('fluent_community/user/password_changed', $user->ID); return [ 'message' => __('Your password has been changed successfully', 'fluent-community'), 'rest_nonce' => wp_create_nonce('wp_rest'), 'ajax_nonce' => wp_create_nonce('fluent_community_ajax_nonce'), ]; } public function getAllMemberships(Request $request, $userName) { /** @var XProfile $xProfile */ $xProfile = XProfile::where('username', $userName)->firstOrFail(); $currentUser = $this->getUser(); if (!ProfileHelper::canViewUserSpaces($xProfile->user_id, $currentUser)) { return $this->sendError([ 'message' => __('You are not allowed to view this profile\'s membership.', 'fluent-community'), 'permission_failed' => true ]); } $canSeeSecret = $xProfile->user_id == get_current_user_id() || ($currentUser && $currentUser->isCommunityModerator()); $memberships = $xProfile->spaces() ->wherePivot('status', 'active') ->when(!$canSeeSecret, function ($q) { $q->whereIn('privacy', ['public', 'private']); }) ->get() ->pluck('id'); return apply_filters('fluent_community/profile_all_memberships_api_response', [ 'memberships' => $memberships ], $request->all()); } public function getSpaces(Request $request, $userName) { /** @var XProfile $xProfile */ $xProfile = XProfile::where('username', $userName)->firstOrFail(); $currentUser = $this->getUser(); if (!ProfileHelper::canViewUserSpaces($xProfile->user_id, $currentUser)) { return $this->sendError([ 'message' => __('You are not allowed to view this profile\'s spaces.', 'fluent-community'), 'permission_failed' => true ]); } if ($xProfile->user_id == get_current_user_id() || ($currentUser && $currentUser->isCommunityModerator())) { $spaces = $xProfile->spaces() ->wherePivot('status', 'active') ->get(); } else { $spaces = $xProfile->spaces() ->whereIn('privacy', ['public', 'private']) ->wherePivot('status', 'active') ->get(); } foreach ($spaces as $space) { $shouldHideMembersCount = Arr::get($space->settings, 'hide_members_count') == 'yes'; $canViewMembers = $currentUser && $space->verifyUserPermisson($currentUser, 'can_view_members', false); if ($shouldHideMembersCount && !$canViewMembers) { $space->members_count = 0; continue; } $space->members_count = $space->members()->count(); } $data = [ 'spaces' => $spaces ]; return apply_filters('fluent_community/profile_spaces_api_response', $data, $request->all()); } public function getCourses(Request $request, $userName) { if (!Helper::isFeatureEnabled('course_module')) { return $this->sendError([ 'message' => __('Course module is disabled.', 'fluent-community') ]); } /** @var XProfile $xProfile */ $xProfile = XProfile::where('username', $userName)->firstOrFail(); $currentUser = $this->getUser(); if (!ProfileHelper::canViewUserSpaces($xProfile->user_id, $currentUser)) { return $this->sendError([ 'message' => __('You are not allowed to view this profile\'s courses.', 'fluent-community'), 'permission_failed' => true ]); } $hasAllAccess = $xProfile->user_id == get_current_user_id() || ($currentUser && $currentUser->isCommunityModerator()); $courses = $xProfile->courses() ->wherePivot('status', 'active') ->where('fcom_spaces.status', 'published') ->when(!$hasAllAccess, function ($q) { $q->whereIn('fcom_spaces.privacy', ['public', 'private']); }) ->get(); foreach ($courses as $course) { $course->isEnrolled = CourseHelper::isEnrolled($course->id, $xProfile->user_id); if ($course->isEnrolled) { $course->progress = CourseHelper::getCourseProgress($course->id, $xProfile->user_id); } if (!$course->cover_photo) { $course->cover_photo = FLUENT_COMMUNITY_PLUGIN_URL . 'assets/images/course-placeholder.jpg'; } $course->sectionsCount = CourseTopic::where('space_id', $course->id)->count(); $course->lessonsCount = CourseLesson::where('space_id', $course->id)->count(); if (Arr::get($course->settings, 'hide_members_count') != 'yes') { $course->studentsCount = SpaceUserPivot::where('space_id', $course->id)->count(); } else { $course->studentsCount = 0; } do_action_ref_array('fluent_community/course', [&$course]); } $data = [ 'courses' => $courses ]; return apply_filters('fluent_community/profile_courses_api_response', $data, $request->all()); } public function getComments(Request $request, $userName) { $xProfile = XProfile::where('username', $userName)->first(); if (!$xProfile) { return $this->sendError([ 'message' => __('Profile not found', 'fluent-community') ]); } $currentUser = $this->getUser(); $hasAllAccess = $xProfile->user_id == get_current_user_id() || ($currentUser && $currentUser->isCommunityModerator()); $comments = Comment::where('user_id', $xProfile->user_id) ->where('status', 'published') ->with([ 'post' => function ($q) use ($currentUser) { // Eager load the full feed so the post opens in the modal without a per-click fetch. $q->select(array_merge(Feed::$publicColumns, ['message'])) ->with(Feed::withPublicRelations($currentUser)); } ]) ->when(!$hasAllAccess, function ($q) { $q->whereHas('post', function ($query) { $query->byUserAccess(get_current_user_id()); $query->where('type', 'text'); }); }) ->orderBy('id', 'desc') ->paginate(); $posts = $comments->getCollection() ->pluck('post') ->filter() ->unique('id') ->values(); if ($posts->isNotEmpty()) { FeedsHelper::transformFeedsCollection($posts); } $data = [ 'comments' => $comments, 'xprofile' => $xProfile ]; return apply_filters('fluent_community/profile_comments_api_response', $data, $request->all()); } public function getNotificationPreferance(Request $request, $userName) { $emailPref = Utility::getEmailNotificationSettings(); $xProfile = $this->verifyAndGetProfile($userName); $globalPreferances = NotificationPref::getGlobalPrefs(); // Read through the same service the save path writes through. These rows // live in fcom_notification_prefs, keyed by flat keys - space-scoped ones // carry an '_' suffix. $userPrefs = NotificationPref::getUserPrefs($xProfile->user_id); $frequencyMaps = [ 0 => 'disabled', 1 => 'hourly', 2 => 'daily', 3 => 'weekly' ]; $userGlobalPrefs = []; $spaceWisePrefs = []; foreach ($userPrefs as $prefKey => $prefValue) { if ($prefKey === 'message_email_frequency') { $userGlobalPrefs[$prefKey] = isset($frequencyMaps[$prefValue]) ? $frequencyMaps[$prefValue] : 'default'; continue; } if (preg_match('/^(np_by_(?:member|admin)_mail)_(\d+)$/', $prefKey, $matches)) { $spaceId = (int)$matches[2]; if (empty($spaceWisePrefs[$spaceId])) { $spaceWisePrefs[$spaceId] = []; } $spaceWisePrefs[$spaceId][$matches[1]] = $prefValue; continue; } $userGlobalPrefs[$prefKey] = $prefValue ? 'yes' : 'no'; } $messagingConfig = Utility::getOption('_messaging_settings', []); $isGlobalPerUser = Arr::get($messagingConfig, 'messaging_email_frequency') == 'disabled'; $pushAvailable = PushNotificationModule::isAvailable(); $userGlobalPrefsDefaults = [ 'digest_mail' => Arr::get($globalPreferances, 'digest_email_status') ? 'yes' : 'no', 'mention_mail' => Arr::get($globalPreferances, 'mention_mail') ? 'yes' : 'no', 'reply_my_com_mail' => Arr::get($globalPreferances, 'reply_my_com_mail') ? 'yes' : 'no', 'com_my_post_mail' => Arr::get($globalPreferances, 'com_my_post_mail') ? 'yes' : 'no', 'message_email_frequency' => $isGlobalPerUser ? 'disabled' : 'default' ]; if ($pushAvailable) { $pushPreferances = NotificationPref::getGlobalPrefs('push'); $userGlobalPrefsDefaults['com_my_post_push'] = Arr::get($pushPreferances, 'com_my_post_push') ? 'yes' : 'no'; $userGlobalPrefsDefaults['reply_my_com_push'] = Arr::get($pushPreferances, 'reply_my_com_push') ? 'yes' : 'no'; $userGlobalPrefsDefaults['mention_push'] = Arr::get($pushPreferances, 'mention_push') ? 'yes' : 'no'; $userGlobalPrefsDefaults['co_com_push'] = Arr::get($pushPreferances, 'co_com_push') ? 'yes' : 'no'; } $userGlobalPrefs = wp_parse_args($userGlobalPrefs, $userGlobalPrefsDefaults); $profileUserId = $xProfile->user_id; $spaceGroups = SpaceGroup::with(['spaces' => function ($query) use ($profileUserId) { $query->whereHas('members', function ($q) use ($profileUserId) { $q->where('user_id', $profileUserId) ->where('status', 'active'); }) ->where('type', 'community'); }]) ->orderBy('serial', 'ASC') ->get(); $formattedSpaceGroups = []; foreach ($spaceGroups as $group) { if ($group->spaces->isEmpty()) { continue; } $formattedSpaces = []; foreach ($group->spaces as $space) { $pref = ''; if (isset($spaceWisePrefs[$space->id])) { $perfs = (array)$spaceWisePrefs[$space->id]; if (!empty($perfs['np_by_member_mail'])) { $pref = 'all_member_posts'; } else if (!empty($perfs['np_by_admin_mail'])) { $pref = 'admin_only_posts'; } } $formattedSpaces[] = [ 'id' => $space->id, 'title' => $space->title, 'icon' => $space->getIconMark(), 'pref' => $pref ]; } if ($formattedSpaces) { $formattedSpaceGroups[] = [ 'id' => $group->id, 'title' => $group->title, 'spaces' => $formattedSpaces ]; } } // let's find the other spaces $otherSpaces = Space::whereHas('members', function ($q) use ($xProfile) { $q->where('user_id', $xProfile->user_id); }) ->whereNull('parent_id') ->orderBy('title', 'ASC') ->get(); if (!$otherSpaces->isEmpty()) { $formattedSpaces = []; foreach ($otherSpaces as $space) { $pref = ''; if (isset($spaceWisePrefs[$space->id])) { $perfs = (array)$spaceWisePrefs[$space->id]; if (!empty($perfs['np_by_member_mail'])) { $pref = 'all_member_posts'; } else if (!empty($perfs['np_by_admin_mail'])) { $pref = 'admin_only_posts'; } } $formattedSpaces[] = [ 'id' => $space->id, 'title' => $space->title, 'icon' => $space->getIconMark(), 'pref' => $pref ]; } $formattedSpaceGroups[] = [ 'id' => 'other_space_group', 'title' => __('Other Spaces', 'fluent-community'), 'spaces' => $formattedSpaces ]; } $digestDay = (string)Arr::get($emailPref, 'digest_mail_day', 'tue'); if ($digestDay) { $maps = [ 'mon' => __('Monday', 'fluent-community'), 'tue' => __('Tuesday', 'fluent-community'), 'wed' => __('Wednesday', 'fluent-community'), 'thu' => __('Thursday', 'fluent-community'), 'fri' => __('Friday', 'fluent-community'), 'sat' => __('Saturday', 'fluent-community'), 'sun' => __('Sunday', 'fluent-community'), ]; if (isset($maps[$digestDay])) { $digestDay = $maps[$digestDay]; } } $crmEmailStatus = ''; if ($xProfile->user_id == get_current_user_id()) { $profileUser = get_user_by('ID', $xProfile->user_id); if ($profileUser && $profileUser->user_email) { $crmEmailStatus = Helper::getCrmUndeliverableStatus($profileUser->user_email); } } $data = [ 'user_globals' => (object)$userGlobalPrefs, 'spaceGroups' => $formattedSpaceGroups, 'space_prefs' => $spaceWisePrefs, 'digestEmailDay' => $digestDay, 'default_messaging_email_frequency' => Arr::get($messagingConfig, 'messaging_email_status') !== 'yes' ? 'no' : Arr::get($messagingConfig, 'messaging_email_frequency'), 'crm_email_status' => $crmEmailStatus, 'push_available' => $pushAvailable, ]; return apply_filters('fluent_community/profile_notification_pref_api_response', $data, $request->all()); } public function saveNotificationPreferance(Request $request, $userName) { $xProfile = $this->verifyAndGetOwnProfile($userName); $userPrefs = $request->get('user_globals', []); $sapcePrefs = $request->get('space_prefs', []); $messagingPref = Arr::get($userPrefs, 'message_email_frequency'); $userPrefs = array_map(function ($item) { return $item == 'yes' ? 1 : 0; }, $userPrefs); if ($messagingPref == 'hourly') { $userPrefs['message_email_frequency'] = 1; } else if ($messagingPref == 'daily') { $userPrefs['message_email_frequency'] = 2; } else if ($messagingPref == 'disabled') { $userPrefs['message_email_frequency'] = 0; } else if ($messagingPref == 'weekly') { $userPrefs['message_email_frequency'] = 3; } else { unset($userPrefs['message_email_frequency']); } foreach ($sapcePrefs as $spaceId => $pref) { $spaceId = (int)$spaceId; if (!$pref || !$spaceId) { continue; } if ($pref == 'all_member_posts') { $userPrefs['np_by_member_mail_' . $spaceId] = 1; $userPrefs['np_by_admin_mail_' . $spaceId] = 1; } else if ($pref == 'admin_only_posts') { $userPrefs['np_by_admin_mail_' . $spaceId] = 1; } } NotificationPref::updateUserPrefs($xProfile->user_id, $userPrefs); return [ 'prefs' => $userPrefs, 'message' => __('Email Notification preferences have been updated', 'fluent-community') ]; } public function reconfirmEmail(Request $request, $userName) { if (!defined('FLUENTCRM')) { return $this->sendError([ 'message' => __('FluentCRM is not available on this site', 'fluent-community') ]); } $xProfile = XProfile::where('username', $userName)->firstOrFail(); if ($xProfile->user_id != get_current_user_id()) { return $this->sendError([ 'message' => __('You can only re-confirm your own email address', 'fluent-community') ]); } $profileUser = get_user_by('ID', $xProfile->user_id); $email = $profileUser ? $profileUser->user_email : ''; if (!$email || !Helper::getCrmUndeliverableStatus($email)) { return $this->sendError([ 'message' => __('Your email address does not need re-confirmation', 'fluent-community') ]); } $subscriber = \FluentCrm\App\Models\Subscriber::where('email', $email)->first(); if (!$subscriber) { return $this->sendError([ 'message' => __('Your email address does not need re-confirmation', 'fluent-community') ]); } // In-memory only, never saved: the opt-in sender is gated on status == 'pending' // and does not persist the subscriber, so the stored status stays untouched // and FluentCommunity keeps pausing emails until the confirmation link is clicked. $subscriber->status = 'pending'; if (!$subscriber->sendDoubleOptinEmail()) { return $this->sendError([ 'message' => __('The confirmation email could not be sent right now. Please try again after a few minutes.', 'fluent-community') ]); } return [ 'message' => __('A confirmation email has been sent. Please check your inbox and click the confirmation link to resume email notifications.', 'fluent-community') ]; } private function verifyAndGetProfile($userName) { $xProfile = XProfile::where('username', $userName)->firstOrFail(); $currentUserId = get_current_user_id(); if ($xProfile->user_id != $currentUserId && !Helper::isSuperAdmin($currentUserId)) { throw new \Exception('You are not allowed to update this profile'); } return $xProfile; } private function verifyAndGetOwnProfile($userName) { $xProfile = XProfile::where('username', $userName)->firstOrFail(); if (!get_current_user_id() || $xProfile->user_id != get_current_user_id()) { throw new HttpException(403, esc_html__('You are not allowed to access these notification preferences.', 'fluent-community')); } return $xProfile; } }