PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.10.0
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.10.0
2.10.0 2.10.01 2.9.1 2.9.0 2.8.1 2.8.0 2.7.7 2.7.5 2.7.0 2.6.01 2.6.0 2.5.0 2.4.01 trunk 1.0.90 1.0.91 1.0.92 1.0.93 1.0.94 1.0.95 1.0.96 1.0.97 1.0.98 1.0.99 1.1.0 All 77 releases
← All changes | app/Http/Controllers/ProfileController.php +348 -62 2.4.012.10.0 View file →
@@ -3,12 +3,12 @@
3 3 namespace FluentCommunity\App\Http\Controllers;
4 4
5 5 use FluentCommunity\App\Functions\Utility;
6 6 use FluentCommunity\App\Models\Comment;
7 -use FluentCommunity\App\Models\NotificationSubscription;
7 +use FluentCommunity\App\Models\Feed;
8 8 use FluentCommunity\App\Models\Space;
9 9 use FluentCommunity\App\Models\SpaceGroup;
10 -use FluentCommunity\App\Models\User;
10 +use FluentCommunity\App\Models\SpaceUserPivot;
11 11 use FluentCommunity\App\Models\XProfile;
12 12 use FluentCommunity\App\Services\CustomSanitizer;
13 13 use FluentCommunity\App\Services\FeedsHelper;
14 14 use FluentCommunity\App\Services\Helper;
@@ -15,21 +15,26 @@
15 15 use FluentCommunity\App\Services\NotificationPref;
16 16 use FluentCommunity\App\Services\ProfileHelper;
17 17 use FluentCommunity\Framework\Http\Request\Request;
18 18 use FluentCommunity\Framework\Support\Arr;
19 +use FluentCommunity\Modules\Course\Model\CourseLesson;
20 +use FluentCommunity\Modules\Course\Model\CourseTopic;
21 +use FluentCommunity\Modules\Course\Services\CourseHelper;
22 +use FluentCommunity\Modules\PushNotification\PushNotificationModule;
23 +use FluentCommunity\Framework\Foundation\Exceptions\HttpException;
19 24
20 25 class ProfileController extends Controller
21 26 {
22 27 public function getProfile(Request $request, $userName)
23 28 {
29 + /** @var XProfile $xprofile */
24 30 $xprofile = XProfile::where('username', $userName)
25 31 ->firstOrFail();
26 32
27 33 if ($xprofile->status != 'active' && !Helper::isModerator()) {
28 34 return $this->sendError([
29 - 'message' => __('This profile is not active', 'fluent-community'),
30 - 'status' => 403
31 - ]);
35 + 'message' => __('This profile is not active', 'fluent-community')
36 + ], 403);
32 37 }
33 38
34 39 $canViewProfile = Utility::canViewUserProfile($xprofile->user_id);
35 40
@@ -40,9 +45,11 @@
40 45 'is_verified' => $xprofile->is_verified,
41 46 'display_name' => $xprofile->display_name,
42 47 'username' => $xprofile->username,
43 48 'avatar' => $xprofile->avatar,
49 + 'has_custom_avatar' => $xprofile->hasCustomAvatar(),
44 50 'cover_photo' => Arr::get($xprofile->meta, 'cover_photo'),
51 + 'headline' => Arr::get($xprofile->meta, 'headline', ''),
45 52 'total_points' => $xprofile->total_points,
46 53 'badge_slugs' => (array)Arr::get($xprofile->meta, 'badge_slug', []),
47 54 'status' => $xprofile->status,
48 55 'is_restricted' => !$canViewProfile,
@@ -68,11 +75,11 @@
68 75 $isAdmin = Helper::isSiteAdmin($currentUserId);
69 76
70 77 if ($isOwn || $isAdmin) {
71 78 $enableUserSync = Utility::getPrivacySetting('enable_user_sync') === 'yes';
72 - $nameArray = explode(' ', trim($xprofile->display_name));
73 - $xprofileLastName = array_pop($nameArray);
74 - $xprofileFirstName = implode(' ', $nameArray);
79 + $nameArray = explode(' ', trim((string) $xprofile->display_name));
80 + $xprofileFirstName = array_shift($nameArray);
81 + $xprofileLastName = implode(' ', $nameArray);
75 82
76 83 $profile['email'] = $user->user_email;
77 84 $profile['first_name'] = $enableUserSync ? $user->first_name : $xprofileFirstName;
78 85 $profile['last_name'] = $enableUserSync ? $user->last_name : $xprofileLastName;
@@ -78,8 +85,9 @@
78 85 $profile['last_name'] = $enableUserSync ? $user->last_name : $xprofileLastName;
79 86 $profile['short_description'] = $xprofile->short_description;
80 87 $profile['can_change_username'] = $isAdmin || Utility::getPrivacySetting('can_customize_username') === 'yes';
81 88 $profile['can_change_email'] = current_user_can('edit_users') || (Utility::getPrivacySetting('can_change_email') === 'yes' && $isOwn);
89 + $profile['can_change_password'] = $isOwn && Utility::getPrivacySetting('can_change_password') === 'yes';
82 90 }
83 91
84 92 $profileBaseUrl = Helper::baseUrl('u/' . $xprofile->username . '/');
85 93
@@ -113,8 +121,20 @@
113 121 'route' => [
114 122 'name' => 'user_spaces'
115 123 ]
116 124 ];
125 +
126 + if (Helper::isFeatureEnabled('course_module')) {
127 + $profile['profile_navs'][] = [
128 + 'slug' => 'user_courses',
129 + 'wrapper_class' => 'fcom_profile_courses',
130 + 'title' => __('Courses', 'fluent-community'),
131 + 'url' => $profileBaseUrl . 'courses',
132 + 'route' => [
133 + 'name' => 'user_courses'
134 + ]
135 + ];
136 + }
117 137 }
118 138
119 139 $profile['profile_navs'][] = [
120 140 'slug' => 'user_comments',
@@ -127,9 +147,9 @@
127 147 ];
128 148
129 149 $profile['profile_nav_actions'] = [];
130 150
131 - $profile = apply_filters('fluent_community/profile_view_data', $profile, $xprofile);
151 + $profile = apply_filters('fluent_community/profile_view_data', $profile, $xprofile, $isAdmin);
132 152
133 153 return [
134 154 'profile' => $profile
135 155 ];
@@ -136,9 +156,9 @@
136 156 }
137 157
138 158 public function patchProfile(Request $request, $userName)
139 159 {
140 - $xprofile = $this->verfifyAndGetProfile($userName);
160 + $xprofile = $this->verifyAndGetProfile($userName);
141 161
142 162 $updateData = $request->get('data', []);
143 163
144 164 if (!empty($updateData['status']) && $updateData['status'] === 'deactivated' && $xprofile->status === 'active') {
@@ -182,11 +202,13 @@
182 202 }
183 203
184 204 $deletedMedias = [];
185 205
186 - if (!empty($updateData['avatar'])) {
206 + if (isset($updateData['avatar'])) {
187 207
188 - $deletedMedias[] = $xprofile->avatar;
208 + if ($xprofile->hasCustomAvatar()) {
209 + $deletedMedias[] = Arr::get($xprofile->getAttributes(), 'avatar');
210 + }
189 211
190 212 $xprofile->avatar = $updateData['avatar'];
191 213
192 214 if (defined('FLUENTCRM')) {
@@ -193,13 +215,16 @@
193 215 $contact = $xprofile->contact;
194 216
195 217 if ($contact) {
196 218 $contact->update([
197 - 'avatar' => $updateData['avatar']
219 + 'avatar' => $updateData['avatar'] ?: null
198 220 ]);
199 221 }
200 222 }
201 223
224 + if (empty($updateData['avatar'])) {
225 + Utility::forgetCache('user_avatar_' . $xprofile->user_id);
226 + }
202 227 }
203 228
204 229 if (isset($updateData['cover_photo'])) {
205 230 $deletedMedias[] = Arr::get($xprofile->meta, 'cover_photo');
@@ -224,8 +249,9 @@
224 249 {
225 250 $currentUser = $this->getUser(true);
226 251 $data = $request->get('data', []);
227 252
253 + /** @var XProfile $xProfile */
228 254 $xProfile = XProfile::where('username', $userName)->firstOrFail();
229 255
230 256 if ($xProfile->user_id != get_current_user_id()) {
231 257 if(!$currentUser->isCommunityModerator()) {
@@ -242,11 +268,10 @@
242 268 ]);
243 269
244 270 $updateData = Arr::only($data, ['first_name', 'last_name', 'short_description', 'website']);
245 271
246 - $updateData = apply_filters('fluent_community/update_profile_data', $updateData, $data, $xProfile);
272 + $updateData = apply_filters('fluent_community/update_profile_data', $updateData, $data, $xProfile, $currentUser);
247 273
248 - $currentUser = User::findOrFail(get_current_user_id());
249 274 $meta = $xProfile->meta;
250 275
251 276 $userNameChanged = false;
252 277
@@ -287,10 +312,13 @@
287 312 $userNameChanged = $userName != $xProfile->username;
288 313 }
289 314
290 315 if (Helper::isFeatureEnabled('user_badge')) {
291 - $badgeSlug = (array)Arr::get($data, 'badge_slugs', []);
292 - $meta['badge_slug'] = $badgeSlug;
316 + $badgeSlug = array_filter((array) Arr::get($data, 'badge_slugs', []), 'is_scalar');
317 + $badgeSlug = array_map('sanitize_text_field', $badgeSlug);
318 +
319 + $definedBadges = (array) Utility::getOption('user_badges', []);
320 + $meta['badge_slug'] = array_values(array_intersect($badgeSlug, array_keys($definedBadges)));
293 321 }
294 322 } else if (Utility::getPrivacySetting('can_customize_username')) {
295 323 $userName = Arr::get($data, 'username');
296 324
@@ -327,10 +355,11 @@
327 355 }
328 356
329 357 $updateData['display_name'] = trim(sanitize_text_field(Arr::get($data, 'first_name') . ' ' . Arr::get($data, 'last_name')));
330 358
331 - $updateData['short_description'] = CustomSanitizer::unslashMarkdown(sanitize_textarea_field(trim(Arr::get($data, 'short_description'))));
332 - $meta['website'] = sanitize_url(Arr::get($data, 'website'));
359 + $updateData['short_description'] = CustomSanitizer::unslashMarkdown(sanitize_textarea_field(trim((string) Arr::get($data, 'short_description', ''))));
360 + $meta['website'] = sanitize_url((string) Arr::get($data, 'website', ''));
361 + $meta['headline'] = sanitize_text_field(trim(Arr::get($data, 'headline', '')));
333 362 $socialLinks = Arr::get($data, 'social_links', []);
334 363
335 364 $maxDescriptionLength = apply_filters('fluent_community/max_profile_description_length', 5000);
336 365 if ($updateData['short_description'] && strlen($updateData['short_description']) > $maxDescriptionLength) {
@@ -342,8 +371,19 @@
342 371 )
343 372 ]);
344 373 }
345 374
375 + $maxHeadlineLength = apply_filters('fluent_community/max_profile_headline_length', 60);
376 + if ($meta['headline'] && mb_strlen($meta['headline']) > $maxHeadlineLength) {
377 + return $this->sendError([
378 + 'message' => sprintf(
379 + /* translators: %d: Maximum number of characters allowed in the profile headline. */
380 + __('Headline should not exceed %d characters.', 'fluent-community'),
381 + $maxHeadlineLength
382 + )
383 + ]);
384 + }
385 +
346 386 if ($socialLinks) {
347 387 $socialLinks = array_filter($socialLinks);
348 388 $formattedSocialLinkes = [];
349 389 $socialLinkProviders = ProfileHelper::socialLinkProviders(true);
@@ -410,10 +450,100 @@
410 450 'profile' => $xProfile
411 451 ];
412 452 }
413 453
454 + public function changePassword(Request $request, $userName)
455 + {
456 + $xProfile = XProfile::where('username', $userName)->firstOrFail();
457 +
458 + // Password can only be changed by the account owner, never by moderators/admins here.
459 + if ($xProfile->user_id != get_current_user_id()) {
460 + return $this->sendError([
461 + 'message' => __('You are not allowed to change this password', 'fluent-community')
462 + ]);
463 + }
464 +
465 + if (Utility::getPrivacySetting('can_change_password') !== 'yes') {
466 + return $this->sendError([
467 + 'message' => __('Password change is disabled', 'fluent-community')
468 + ]);
469 + }
470 +
471 + $data = $request->get('data', []);
472 +
473 + $this->validate($data, [
474 + 'current_password' => 'required',
475 + 'new_password' => 'required',
476 + 'confirm_password' => 'required',
477 + ], [
478 + 'current_password.required' => __('Current password is required', 'fluent-community'),
479 + 'new_password.required' => __('New password is required', 'fluent-community'),
480 + 'confirm_password.required' => __('Please confirm your new password', 'fluent-community'),
481 + ]);
482 +
483 + // Passwords are used verbatim; sanitizing would corrupt valid characters.
484 + $currentPassword = (string) Arr::get($data, 'current_password');
485 + $newPassword = (string) Arr::get($data, 'new_password');
486 + $confirmPassword = (string) Arr::get($data, 'confirm_password');
487 +
488 + if (strlen($newPassword) < 4) {
489 + return $this->sendError([
490 + 'message' => __('New password must be at least 4 characters long', 'fluent-community')
491 + ]);
492 + }
493 +
494 + if ($newPassword !== $confirmPassword) {
495 + return $this->sendError([
496 + 'message' => __('New password and confirmation do not match', 'fluent-community')
497 + ]);
498 + }
499 +
500 + if ($newPassword === $currentPassword) {
501 + return $this->sendError([
502 + 'message' => __('New password must be different from your current password', 'fluent-community')
503 + ]);
504 + }
505 +
506 + $user = get_user_by('id', $xProfile->user_id);
507 +
508 + if (!$user || !wp_check_password($currentPassword, $user->user_pass, $user->ID)) {
509 + return $this->sendError([
510 + 'message' => __('Your current password is incorrect', 'fluent-community')
511 + ]);
512 + }
513 +
514 + wp_set_password($newPassword, $user->ID);
515 +
516 + // wp_set_password destroys every session for the user, which also invalidates the
517 + // REST nonce the SPA holds. Re-issue the cookie to keep the session, capturing the
518 + // fresh logged-in cookie so the nonces we mint below bind to the new session token.
519 + $newLoggedInCookie = '';
520 + $captureLoggedInCookie = function ($loggedInCookie) use (&$newLoggedInCookie) {
521 + $newLoggedInCookie = $loggedInCookie;
522 + };
523 + add_action('set_logged_in_cookie', $captureLoggedInCookie);
524 +
525 + wp_set_current_user($user->ID);
526 + wp_set_auth_cookie($user->ID, true);
527 +
528 + remove_action('set_logged_in_cookie', $captureLoggedInCookie);
529 +
530 + if ($newLoggedInCookie) {
531 + $_COOKIE[LOGGED_IN_COOKIE] = $newLoggedInCookie;
532 + }
533 +
534 + do_action('fluent_community/user/password_changed', $user->ID);
535 +
536 + return [
537 + 'message' => __('Your password has been changed successfully', 'fluent-community'),
538 + 'rest_nonce' => wp_create_nonce('wp_rest'),
539 + 'ajax_nonce' => wp_create_nonce('fluent_community_ajax_nonce'),
540 + ];
541 + }
542 +
414 543 public function getAllMemberships(Request $request, $userName)
415 544 {
545 + /** @var XProfile $xProfile */
416 546 $xProfile = XProfile::where('username', $userName)->firstOrFail();
417 547
418 548 $currentUser = $this->getUser();
419 549
@@ -423,12 +553,19 @@
423 553 'permission_failed' => true
424 554 ]);
425 555 }
426 556
427 - $memberships = $xProfile->space_pivot()
428 - ->where('status', 'active')
429 - ->pluck('space_id');
557 + $canSeeSecret = $xProfile->user_id == get_current_user_id()
558 + || ($currentUser && $currentUser->isCommunityModerator());
430 559
560 + $memberships = $xProfile->spaces()
561 + ->wherePivot('status', 'active')
562 + ->when(!$canSeeSecret, function ($q) {
563 + $q->whereIn('privacy', ['public', 'private']);
564 + })
565 + ->get()
566 + ->pluck('id');
567 +
431 568 return apply_filters('fluent_community/profile_all_memberships_api_response', [
432 569 'memberships' => $memberships
433 570 ], $request->all());
434 571 }
@@ -434,8 +571,9 @@
434 571 }
435 572
436 573 public function getSpaces(Request $request, $userName)
437 574 {
575 + /** @var XProfile $xProfile */
438 576 $xProfile = XProfile::where('username', $userName)->firstOrFail();
439 577 $currentUser = $this->getUser();
440 578
441 579 if (!ProfileHelper::canViewUserSpaces($xProfile->user_id, $currentUser)) {
@@ -468,12 +606,69 @@
468 606
469 607 $data = [
470 608 'spaces' => $spaces
471 609 ];
472 -
610 +
473 611 return apply_filters('fluent_community/profile_spaces_api_response', $data, $request->all());
474 612 }
475 613
614 + public function getCourses(Request $request, $userName)
615 + {
616 + if (!Helper::isFeatureEnabled('course_module')) {
617 + return $this->sendError([
618 + 'message' => __('Course module is disabled.', 'fluent-community')
619 + ]);
620 + }
621 +
622 + /** @var XProfile $xProfile */
623 + $xProfile = XProfile::where('username', $userName)->firstOrFail();
624 + $currentUser = $this->getUser();
625 +
626 + if (!ProfileHelper::canViewUserSpaces($xProfile->user_id, $currentUser)) {
627 + return $this->sendError([
628 + 'message' => __('You are not allowed to view this profile\'s courses.', 'fluent-community'),
629 + 'permission_failed' => true
630 + ]);
631 + }
632 +
633 + $hasAllAccess = $xProfile->user_id == get_current_user_id() || ($currentUser && $currentUser->isCommunityModerator());
634 +
635 + $courses = $xProfile->courses()
636 + ->wherePivot('status', 'active')
637 + ->where('fcom_spaces.status', 'published')
638 + ->when(!$hasAllAccess, function ($q) {
639 + $q->whereIn('fcom_spaces.privacy', ['public', 'private']);
640 + })
641 + ->get();
642 +
643 + foreach ($courses as $course) {
644 + $course->isEnrolled = CourseHelper::isEnrolled($course->id, $xProfile->user_id);
645 + if ($course->isEnrolled) {
646 + $course->progress = CourseHelper::getCourseProgress($course->id, $xProfile->user_id);
647 + }
648 +
649 + if (!$course->cover_photo) {
650 + $course->cover_photo = FLUENT_COMMUNITY_PLUGIN_URL . 'assets/images/course-placeholder.jpg';
651 + }
652 +
653 + $course->sectionsCount = CourseTopic::where('space_id', $course->id)->count();
654 + $course->lessonsCount = CourseLesson::where('space_id', $course->id)->count();
655 + if (Arr::get($course->settings, 'hide_members_count') != 'yes') {
656 + $course->studentsCount = SpaceUserPivot::where('space_id', $course->id)->count();
657 + } else {
658 + $course->studentsCount = 0;
659 + }
660 +
661 + do_action_ref_array('fluent_community/course', [&$course]);
662 + }
663 +
664 + $data = [
665 + 'courses' => $courses
666 + ];
667 +
668 + return apply_filters('fluent_community/profile_courses_api_response', $data, $request->all());
669 + }
670 +
476 671 public function getComments(Request $request, $userName)
477 672 {
478 673 $xProfile = XProfile::where('username', $userName)->first();
479 674
@@ -488,15 +683,12 @@
488 683
489 684 $comments = Comment::where('user_id', $xProfile->user_id)
490 685 ->where('status', 'published')
491 686 ->with([
492 - 'post' => function ($q) {
493 - $q->select(['id', 'title', 'message', 'type', 'space_id', 'slug', 'created_at'])
494 - ->with([
495 - 'space' => function ($q) {
496 - $q->select(['id', 'title', 'slug', 'type']);
497 - }
498 - ]);
687 + 'post' => function ($q) use ($currentUser) {
688 + // Eager load the full feed so the post opens in the modal without a per-click fetch.
689 + $q->select(array_merge(Feed::$publicColumns, ['message']))
690 + ->with(Feed::withPublicRelations($currentUser));
499 691 }
500 692 ])
501 693 ->when(!$hasAllAccess, function ($q) {
502 694 $q->whereHas('post', function ($query) {
@@ -506,8 +698,18 @@
506 698 })
507 699 ->orderBy('id', 'desc')
508 700 ->paginate();
509 701
702 + $posts = $comments->getCollection()
703 + ->pluck('post')
704 + ->filter()
705 + ->unique('id')
706 + ->values();
707 +
708 + if ($posts->isNotEmpty()) {
709 + FeedsHelper::transformFeedsCollection($posts);
710 + }
711 +
510 712 $data = [
511 713 'comments' => $comments,
512 714 'xprofile' => $xProfile
513 715 ];
@@ -518,47 +720,51 @@
518 720 public function getNotificationPreferance(Request $request, $userName)
519 721 {
520 722 $emailPref = Utility::getEmailNotificationSettings();
521 723
522 - $xProfile = $this->verfifyAndGetProfile($userName);
724 + $xProfile = $this->verifyAndGetProfile($userName);
523 725
524 726 $globalPreferances = NotificationPref::getGlobalPrefs();
525 727
526 - $userPrefs = NotificationSubscription::where('user_id', $xProfile->user_id)
527 - ->select(['notification_type', 'is_read', 'object_id'])
528 - ->get();
728 + // Read through the same service the save path writes through. These rows
729 + // live in fcom_notification_prefs, keyed by flat keys - space-scoped ones
730 + // carry an '_<space id>' suffix.
731 + $userPrefs = NotificationPref::getUserPrefs($xProfile->user_id);
529 732
733 + $frequencyMaps = [
734 + 0 => 'disabled',
735 + 1 => 'hourly',
736 + 2 => 'daily',
737 + 3 => 'weekly'
738 + ];
739 +
530 740 $userGlobalPrefs = [];
531 741 $spaceWisePrefs = [];
532 - foreach ($userPrefs as $pref) {
533 - if (!$pref->object_id) {
534 - if ($pref->notification_type === 'message_email_frequency') {
535 - $maps = [
536 - 0 => 'disabled',
537 - 1 => 'hourly',
538 - 2 => 'daily',
539 - 3 => 'weekly'
540 - ];
742 + foreach ($userPrefs as $prefKey => $prefValue) {
743 + if ($prefKey === 'message_email_frequency') {
744 + $userGlobalPrefs[$prefKey] = isset($frequencyMaps[$prefValue]) ? $frequencyMaps[$prefValue] : 'default';
745 + continue;
746 + }
541 747
542 - if ($maps[$pref->is_read]) {
543 - $userGlobalPrefs[$pref->notification_type] = $maps[$pref->is_read];
544 - } else {
545 - $userGlobalPrefs[$pref->notification_type] = 'default';
546 - }
547 - continue;
748 + if (preg_match('/^(np_by_(?:member|admin)_mail)_(\d+)$/', $prefKey, $matches)) {
749 + $spaceId = (int)$matches[2];
750 +
751 + if (empty($spaceWisePrefs[$spaceId])) {
752 + $spaceWisePrefs[$spaceId] = [];
548 753 }
549 - $userGlobalPrefs[$pref->notification_type] = $pref->is_read ? 'yes' : 'no';
550 - } else {
551 - if (empty($spaceWisePrefs[$pref->object_id])) {
552 - $spaceWisePrefs[$pref->object_id] = [];
553 - }
554 - $spaceWisePrefs[$pref->object_id][$pref->notification_type] = $pref->is_read;
754 +
755 + $spaceWisePrefs[$spaceId][$matches[1]] = $prefValue;
756 + continue;
555 757 }
758 +
759 + $userGlobalPrefs[$prefKey] = $prefValue ? 'yes' : 'no';
556 760 }
557 761
558 762 $messagingConfig = Utility::getOption('_messaging_settings', []);
559 763 $isGlobalPerUser = Arr::get($messagingConfig, 'messaging_email_frequency') == 'disabled';
560 764
765 + $pushAvailable = PushNotificationModule::isAvailable();
766 +
561 767 $userGlobalPrefsDefaults = [
562 768 'digest_mail' => Arr::get($globalPreferances, 'digest_email_status') ? 'yes' : 'no',
563 769 'mention_mail' => Arr::get($globalPreferances, 'mention_mail') ? 'yes' : 'no',
564 770 'reply_my_com_mail' => Arr::get($globalPreferances, 'reply_my_com_mail') ? 'yes' : 'no',
@@ -565,13 +771,23 @@
565 771 'com_my_post_mail' => Arr::get($globalPreferances, 'com_my_post_mail') ? 'yes' : 'no',
566 772 'message_email_frequency' => $isGlobalPerUser ? 'disabled' : 'default'
567 773 ];
568 774
775 + if ($pushAvailable) {
776 + $pushPreferances = NotificationPref::getGlobalPrefs('push');
777 +
778 + $userGlobalPrefsDefaults['com_my_post_push'] = Arr::get($pushPreferances, 'com_my_post_push') ? 'yes' : 'no';
779 + $userGlobalPrefsDefaults['reply_my_com_push'] = Arr::get($pushPreferances, 'reply_my_com_push') ? 'yes' : 'no';
780 + $userGlobalPrefsDefaults['mention_push'] = Arr::get($pushPreferances, 'mention_push') ? 'yes' : 'no';
781 + $userGlobalPrefsDefaults['co_com_push'] = Arr::get($pushPreferances, 'co_com_push') ? 'yes' : 'no';
782 + }
783 +
569 784 $userGlobalPrefs = wp_parse_args($userGlobalPrefs, $userGlobalPrefsDefaults);
570 785
571 - $spaceGroups = SpaceGroup::with(['spaces' => function ($query) {
572 - $query->whereHas('members', function ($q) {
573 - $q->where('user_id', get_current_user_id())
786 + $profileUserId = $xProfile->user_id;
787 + $spaceGroups = SpaceGroup::with(['spaces' => function ($query) use ($profileUserId) {
788 + $query->whereHas('members', function ($q) use ($profileUserId) {
789 + $q->where('user_id', $profileUserId)
574 790 ->where('status', 'active');
575 791 })
576 792 ->where('type', 'community');
577 793 }])
@@ -663,8 +879,16 @@
663 879 $digestDay = $maps[$digestDay];
664 880 }
665 881 }
666 882
883 + $crmEmailStatus = '';
884 + if ($xProfile->user_id == get_current_user_id()) {
885 + $profileUser = get_user_by('ID', $xProfile->user_id);
886 + if ($profileUser && $profileUser->user_email) {
887 + $crmEmailStatus = Helper::getCrmUndeliverableStatus($profileUser->user_email);
888 + }
889 + }
890 +
667 891 $data = [
668 892 'user_globals' => (object)$userGlobalPrefs,
669 893 'spaceGroups' => $formattedSpaceGroups,
670 894 'space_prefs' => $spaceWisePrefs,
@@ -669,8 +893,10 @@
669 893 'spaceGroups' => $formattedSpaceGroups,
670 894 'space_prefs' => $spaceWisePrefs,
671 895 'digestEmailDay' => $digestDay,
672 896 'default_messaging_email_frequency' => Arr::get($messagingConfig, 'messaging_email_status') !== 'yes' ? 'no' : Arr::get($messagingConfig, 'messaging_email_frequency'),
897 + 'crm_email_status' => $crmEmailStatus,
898 + 'push_available' => $pushAvailable,
673 899 ];
674 900
675 901 return apply_filters('fluent_community/profile_notification_pref_api_response', $data, $request->all());
676 902 }
@@ -676,9 +902,9 @@
676 902 }
677 903
678 904 public function saveNotificationPreferance(Request $request, $userName)
679 905 {
680 - $xProfile = $this->verfifyAndGetProfile($userName);
906 + $xProfile = $this->verifyAndGetOwnProfile($userName);
681 907
682 908 $userPrefs = $request->get('user_globals', []);
683 909 $sapcePrefs = $request->get('space_prefs', []);
684 910
@@ -721,15 +947,75 @@
721 947 'message' => __('Email Notification preferences have been updated', 'fluent-community')
722 948 ];
723 949 }
724 950
725 - private function verfifyAndGetProfile($userName)
951 + public function reconfirmEmail(Request $request, $userName)
726 952 {
953 + if (!defined('FLUENTCRM')) {
954 + return $this->sendError([
955 + 'message' => __('FluentCRM is not available on this site', 'fluent-community')
956 + ]);
957 + }
958 +
727 959 $xProfile = XProfile::where('username', $userName)->firstOrFail();
728 960
729 - $currentUser = $this->getUser();
730 - if ($xProfile->user_id != get_current_user_id() && (!$currentUser || !$currentUser->isCommunityModerator())) {
961 + if ($xProfile->user_id != get_current_user_id()) {
962 + return $this->sendError([
963 + 'message' => __('You can only re-confirm your own email address', 'fluent-community')
964 + ]);
965 + }
966 +
967 + $profileUser = get_user_by('ID', $xProfile->user_id);
968 + $email = $profileUser ? $profileUser->user_email : '';
969 +
970 + if (!$email || !Helper::getCrmUndeliverableStatus($email)) {
971 + return $this->sendError([
972 + 'message' => __('Your email address does not need re-confirmation', 'fluent-community')
973 + ]);
974 + }
975 +
976 + $subscriber = \FluentCrm\App\Models\Subscriber::where('email', $email)->first();
977 +
978 + if (!$subscriber) {
979 + return $this->sendError([
980 + 'message' => __('Your email address does not need re-confirmation', 'fluent-community')
981 + ]);
982 + }
983 +
984 + // In-memory only, never saved: the opt-in sender is gated on status == 'pending'
985 + // and does not persist the subscriber, so the stored status stays untouched
986 + // and FluentCommunity keeps pausing emails until the confirmation link is clicked.
987 + $subscriber->status = 'pending';
988 +
989 + if (!$subscriber->sendDoubleOptinEmail()) {
990 + return $this->sendError([
991 + 'message' => __('The confirmation email could not be sent right now. Please try again after a few minutes.', 'fluent-community')
992 + ]);
993 + }
994 +
995 + return [
996 + 'message' => __('A confirmation email has been sent. Please check your inbox and click the confirmation link to resume email notifications.', 'fluent-community')
997 + ];
998 + }
999 +
1000 + private function verifyAndGetProfile($userName)
1001 + {
1002 + $xProfile = XProfile::where('username', $userName)->firstOrFail();
1003 +
1004 + $currentUserId = get_current_user_id();
1005 + if ($xProfile->user_id != $currentUserId && !Helper::isSuperAdmin($currentUserId)) {
731 1006 throw new \Exception('You are not allowed to update this profile');
1007 + }
1008 +
1009 + return $xProfile;
1010 + }
1011 +
1012 + private function verifyAndGetOwnProfile($userName)
1013 + {
1014 + $xProfile = XProfile::where('username', $userName)->firstOrFail();
1015 +
1016 + if (!get_current_user_id() || $xProfile->user_id != get_current_user_id()) {
1017 + throw new HttpException(403, esc_html__('You are not allowed to access these notification preferences.', 'fluent-community'));
732 1018 }
733 1019
734 1020 return $xProfile;
735 1021 }