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 +246 -51 2.7.02.10.0 View file →
@@ -3,13 +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 10 use FluentCommunity\App\Models\SpaceUserPivot;
11 -use FluentCommunity\App\Models\User;
12 11 use FluentCommunity\App\Models\XProfile;
13 12 use FluentCommunity\App\Services\CustomSanitizer;
14 13 use FluentCommunity\App\Services\FeedsHelper;
15 14 use FluentCommunity\App\Services\Helper;
@@ -19,21 +18,23 @@
19 18 use FluentCommunity\Framework\Support\Arr;
20 19 use FluentCommunity\Modules\Course\Model\CourseLesson;
21 20 use FluentCommunity\Modules\Course\Model\CourseTopic;
22 21 use FluentCommunity\Modules\Course\Services\CourseHelper;
22 +use FluentCommunity\Modules\PushNotification\PushNotificationModule;
23 +use FluentCommunity\Framework\Foundation\Exceptions\HttpException;
23 24
24 25 class ProfileController extends Controller
25 26 {
26 27 public function getProfile(Request $request, $userName)
27 28 {
29 + /** @var XProfile $xprofile */
28 30 $xprofile = XProfile::where('username', $userName)
29 31 ->firstOrFail();
30 32
31 33 if ($xprofile->status != 'active' && !Helper::isModerator()) {
32 34 return $this->sendError([
33 - 'message' => __('This profile is not active', 'fluent-community'),
34 - 'status' => 403
35 - ]);
35 + 'message' => __('This profile is not active', 'fluent-community')
36 + ], 403);
36 37 }
37 38
38 39 $canViewProfile = Utility::canViewUserProfile($xprofile->user_id);
39 40
@@ -74,9 +75,9 @@
74 75 $isAdmin = Helper::isSiteAdmin($currentUserId);
75 76
76 77 if ($isOwn || $isAdmin) {
77 78 $enableUserSync = Utility::getPrivacySetting('enable_user_sync') === 'yes';
78 - $nameArray = explode(' ', trim($xprofile->display_name));
79 + $nameArray = explode(' ', trim((string) $xprofile->display_name));
79 80 $xprofileFirstName = array_shift($nameArray);
80 81 $xprofileLastName = implode(' ', $nameArray);
81 82
82 83 $profile['email'] = $user->user_email;
@@ -84,8 +85,9 @@
84 85 $profile['last_name'] = $enableUserSync ? $user->last_name : $xprofileLastName;
85 86 $profile['short_description'] = $xprofile->short_description;
86 87 $profile['can_change_username'] = $isAdmin || Utility::getPrivacySetting('can_customize_username') === 'yes';
87 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';
88 90 }
89 91
90 92 $profileBaseUrl = Helper::baseUrl('u/' . $xprofile->username . '/');
91 93
@@ -154,9 +156,9 @@
154 156 }
155 157
156 158 public function patchProfile(Request $request, $userName)
157 159 {
158 - $xprofile = $this->verfifyAndGetProfile($userName);
160 + $xprofile = $this->verifyAndGetProfile($userName);
159 161
160 162 $updateData = $request->get('data', []);
161 163
162 164 if (!empty($updateData['status']) && $updateData['status'] === 'deactivated' && $xprofile->status === 'active') {
@@ -203,9 +205,9 @@
203 205
204 206 if (isset($updateData['avatar'])) {
205 207
206 208 if ($xprofile->hasCustomAvatar()) {
207 - $deletedMedias[] = $xprofile->attributes['avatar'];
209 + $deletedMedias[] = Arr::get($xprofile->getAttributes(), 'avatar');
208 210 }
209 211
210 212 $xprofile->avatar = $updateData['avatar'];
211 213
@@ -247,8 +249,9 @@
247 249 {
248 250 $currentUser = $this->getUser(true);
249 251 $data = $request->get('data', []);
250 252
253 + /** @var XProfile $xProfile */
251 254 $xProfile = XProfile::where('username', $userName)->firstOrFail();
252 255
253 256 if ($xProfile->user_id != get_current_user_id()) {
254 257 if(!$currentUser->isCommunityModerator()) {
@@ -309,10 +312,13 @@
309 312 $userNameChanged = $userName != $xProfile->username;
310 313 }
311 314
312 315 if (Helper::isFeatureEnabled('user_badge')) {
313 - $badgeSlug = (array)Arr::get($data, 'badge_slugs', []);
314 - $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)));
315 321 }
316 322 } else if (Utility::getPrivacySetting('can_customize_username')) {
317 323 $userName = Arr::get($data, 'username');
318 324
@@ -349,10 +355,10 @@
349 355 }
350 356
351 357 $updateData['display_name'] = trim(sanitize_text_field(Arr::get($data, 'first_name') . ' ' . Arr::get($data, 'last_name')));
352 358
353 - $updateData['short_description'] = CustomSanitizer::unslashMarkdown(sanitize_textarea_field(trim(Arr::get($data, 'short_description'))));
354 - $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', ''));
355 361 $meta['headline'] = sanitize_text_field(trim(Arr::get($data, 'headline', '')));
356 362 $socialLinks = Arr::get($data, 'social_links', []);
357 363
358 364 $maxDescriptionLength = apply_filters('fluent_community/max_profile_description_length', 5000);
@@ -444,10 +450,100 @@
444 450 'profile' => $xProfile
445 451 ];
446 452 }
447 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 +
448 543 public function getAllMemberships(Request $request, $userName)
449 544 {
545 + /** @var XProfile $xProfile */
450 546 $xProfile = XProfile::where('username', $userName)->firstOrFail();
451 547
452 548 $currentUser = $this->getUser();
453 549
@@ -457,12 +553,19 @@
457 553 'permission_failed' => true
458 554 ]);
459 555 }
460 556
461 - $memberships = $xProfile->space_pivot()
462 - ->where('status', 'active')
463 - ->pluck('space_id');
557 + $canSeeSecret = $xProfile->user_id == get_current_user_id()
558 + || ($currentUser && $currentUser->isCommunityModerator());
464 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 +
465 568 return apply_filters('fluent_community/profile_all_memberships_api_response', [
466 569 'memberships' => $memberships
467 570 ], $request->all());
468 571 }
@@ -468,8 +571,9 @@
468 571 }
469 572
470 573 public function getSpaces(Request $request, $userName)
471 574 {
575 + /** @var XProfile $xProfile */
472 576 $xProfile = XProfile::where('username', $userName)->firstOrFail();
473 577 $currentUser = $this->getUser();
474 578
475 579 if (!ProfileHelper::canViewUserSpaces($xProfile->user_id, $currentUser)) {
@@ -514,8 +618,9 @@
514 618 'message' => __('Course module is disabled.', 'fluent-community')
515 619 ]);
516 620 }
517 621
622 + /** @var XProfile $xProfile */
518 623 $xProfile = XProfile::where('username', $userName)->firstOrFail();
519 624 $currentUser = $this->getUser();
520 625
521 626 if (!ProfileHelper::canViewUserSpaces($xProfile->user_id, $currentUser)) {
@@ -578,15 +683,12 @@
578 683
579 684 $comments = Comment::where('user_id', $xProfile->user_id)
580 685 ->where('status', 'published')
581 686 ->with([
582 - 'post' => function ($q) {
583 - $q->select(['id', 'title', 'message', 'type', 'space_id', 'slug', 'created_at'])
584 - ->with([
585 - 'space' => function ($q) {
586 - $q->select(['id', 'title', 'slug', 'type']);
587 - }
588 - ]);
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));
589 691 }
590 692 ])
591 693 ->when(!$hasAllAccess, function ($q) {
592 694 $q->whereHas('post', function ($query) {
@@ -596,8 +698,18 @@
596 698 })
597 699 ->orderBy('id', 'desc')
598 700 ->paginate();
599 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 +
600 712 $data = [
601 713 'comments' => $comments,
602 714 'xprofile' => $xProfile
603 715 ];
@@ -608,47 +720,51 @@
608 720 public function getNotificationPreferance(Request $request, $userName)
609 721 {
610 722 $emailPref = Utility::getEmailNotificationSettings();
611 723
612 - $xProfile = $this->verfifyAndGetProfile($userName);
724 + $xProfile = $this->verifyAndGetProfile($userName);
613 725
614 726 $globalPreferances = NotificationPref::getGlobalPrefs();
615 727
616 - $userPrefs = NotificationSubscription::where('user_id', $xProfile->user_id)
617 - ->select(['notification_type', 'is_read', 'object_id'])
618 - ->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);
619 732
733 + $frequencyMaps = [
734 + 0 => 'disabled',
735 + 1 => 'hourly',
736 + 2 => 'daily',
737 + 3 => 'weekly'
738 + ];
739 +
620 740 $userGlobalPrefs = [];
621 741 $spaceWisePrefs = [];
622 - foreach ($userPrefs as $pref) {
623 - if (!$pref->object_id) {
624 - if ($pref->notification_type === 'message_email_frequency') {
625 - $maps = [
626 - 0 => 'disabled',
627 - 1 => 'hourly',
628 - 2 => 'daily',
629 - 3 => 'weekly'
630 - ];
742 + foreach ($userPrefs as $prefKey => $prefValue) {
743 + if ($prefKey === 'message_email_frequency') {
744 + $userGlobalPrefs[$prefKey] = isset($frequencyMaps[$prefValue]) ? $frequencyMaps[$prefValue] : 'default';
745 + continue;
746 + }
631 747
632 - if ($maps[$pref->is_read]) {
633 - $userGlobalPrefs[$pref->notification_type] = $maps[$pref->is_read];
634 - } else {
635 - $userGlobalPrefs[$pref->notification_type] = 'default';
636 - }
637 - 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] = [];
638 753 }
639 - $userGlobalPrefs[$pref->notification_type] = $pref->is_read ? 'yes' : 'no';
640 - } else {
641 - if (empty($spaceWisePrefs[$pref->object_id])) {
642 - $spaceWisePrefs[$pref->object_id] = [];
643 - }
644 - $spaceWisePrefs[$pref->object_id][$pref->notification_type] = $pref->is_read;
754 +
755 + $spaceWisePrefs[$spaceId][$matches[1]] = $prefValue;
756 + continue;
645 757 }
758 +
759 + $userGlobalPrefs[$prefKey] = $prefValue ? 'yes' : 'no';
646 760 }
647 761
648 762 $messagingConfig = Utility::getOption('_messaging_settings', []);
649 763 $isGlobalPerUser = Arr::get($messagingConfig, 'messaging_email_frequency') == 'disabled';
650 764
765 + $pushAvailable = PushNotificationModule::isAvailable();
766 +
651 767 $userGlobalPrefsDefaults = [
652 768 'digest_mail' => Arr::get($globalPreferances, 'digest_email_status') ? 'yes' : 'no',
653 769 'mention_mail' => Arr::get($globalPreferances, 'mention_mail') ? 'yes' : 'no',
654 770 'reply_my_com_mail' => Arr::get($globalPreferances, 'reply_my_com_mail') ? 'yes' : 'no',
@@ -655,8 +771,17 @@
655 771 'com_my_post_mail' => Arr::get($globalPreferances, 'com_my_post_mail') ? 'yes' : 'no',
656 772 'message_email_frequency' => $isGlobalPerUser ? 'disabled' : 'default'
657 773 ];
658 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 +
659 784 $userGlobalPrefs = wp_parse_args($userGlobalPrefs, $userGlobalPrefsDefaults);
660 785
661 786 $profileUserId = $xProfile->user_id;
662 787 $spaceGroups = SpaceGroup::with(['spaces' => function ($query) use ($profileUserId) {
@@ -754,8 +879,16 @@
754 879 $digestDay = $maps[$digestDay];
755 880 }
756 881 }
757 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 +
758 891 $data = [
759 892 'user_globals' => (object)$userGlobalPrefs,
760 893 'spaceGroups' => $formattedSpaceGroups,
761 894 'space_prefs' => $spaceWisePrefs,
@@ -760,8 +893,10 @@
760 893 'spaceGroups' => $formattedSpaceGroups,
761 894 'space_prefs' => $spaceWisePrefs,
762 895 'digestEmailDay' => $digestDay,
763 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,
764 899 ];
765 900
766 901 return apply_filters('fluent_community/profile_notification_pref_api_response', $data, $request->all());
767 902 }
@@ -767,9 +902,9 @@
767 902 }
768 903
769 904 public function saveNotificationPreferance(Request $request, $userName)
770 905 {
771 - $xProfile = $this->verfifyAndGetProfile($userName);
906 + $xProfile = $this->verifyAndGetOwnProfile($userName);
772 907
773 908 $userPrefs = $request->get('user_globals', []);
774 909 $sapcePrefs = $request->get('space_prefs', []);
775 910
@@ -812,15 +947,75 @@
812 947 'message' => __('Email Notification preferences have been updated', 'fluent-community')
813 948 ];
814 949 }
815 950
816 - private function verfifyAndGetProfile($userName)
951 + public function reconfirmEmail(Request $request, $userName)
817 952 {
953 + if (!defined('FLUENTCRM')) {
954 + return $this->sendError([
955 + 'message' => __('FluentCRM is not available on this site', 'fluent-community')
956 + ]);
957 + }
958 +
818 959 $xProfile = XProfile::where('username', $userName)->firstOrFail();
819 960
820 - $currentUser = $this->getUser();
821 - 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)) {
822 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'));
823 1018 }
824 1019
825 1020 return $xProfile;
826 1021 }