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 +616 -92 1.0.992.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,45 +15,142 @@
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 {
24 - $xprofile = XProfile::where('username', $userName)->firstOrFail();
29 + /** @var XProfile $xprofile */
30 + $xprofile = XProfile::where('username', $userName)
31 + ->firstOrFail();
25 32
33 + if ($xprofile->status != 'active' && !Helper::isModerator()) {
34 + return $this->sendError([
35 + 'message' => __('This profile is not active', 'fluent-community')
36 + ], 403);
37 + }
38 +
39 + $canViewProfile = Utility::canViewUserProfile($xprofile->user_id);
40 +
26 41 $user = get_user_by('ID', $xprofile->user_id);
27 42
28 43 $profile = [
29 - 'user_id' => $xprofile->user_id,
30 - 'is_verified' => $xprofile->is_verified,
31 - 'display_name' => $xprofile->display_name,
32 - 'username' => $xprofile->username,
33 - 'avatar' => $xprofile->avatar,
34 - 'created_at' => $xprofile->created_at->format('Y-m-d H:i:s'),
35 - 'last_activity' => $xprofile->last_activity,
36 - 'short_description_rendered' => FeedsHelper::mdToHtml($xprofile->short_description),
37 - 'cover_photo' => Arr::get($xprofile->meta, 'cover_photo'),
38 - 'website' => Arr::get($xprofile->meta, 'website'),
39 - 'social_links' => (object)Arr::get($xprofile->meta, 'social_links', []),
40 - 'status' => $xprofile->status,
41 - 'badge_slug' => Arr::get($xprofile->meta, 'badge_slug'),
42 - 'compilation_score' => $xprofile->getCompletionScore(),
43 - 'total_points' => $xprofile->total_points,
44 - 'canViewUserSpaces' => ProfileHelper::canViewUserSpaces($xprofile->user_id, $this->getUser())
44 + 'user_id' => $xprofile->user_id,
45 + 'is_verified' => $xprofile->is_verified,
46 + 'display_name' => $xprofile->display_name,
47 + 'username' => $xprofile->username,
48 + 'avatar' => $xprofile->avatar,
49 + 'has_custom_avatar' => $xprofile->hasCustomAvatar(),
50 + 'cover_photo' => Arr::get($xprofile->meta, 'cover_photo'),
51 + 'headline' => Arr::get($xprofile->meta, 'headline', ''),
52 + 'total_points' => $xprofile->total_points,
53 + 'badge_slugs' => (array)Arr::get($xprofile->meta, 'badge_slug', []),
54 + 'status' => $xprofile->status,
55 + 'is_restricted' => !$canViewProfile,
56 + 'canViewUserSpaces' => ProfileHelper::canViewUserSpaces($xprofile->user_id, $this->getUser())
45 57 ];
46 58
47 - $isAdmin = Helper::isSiteAdmin();
48 - if ($xprofile->user_id == get_current_user_id() || $isAdmin) {
59 + if (Utility::showLastActivity()) {
60 + $profile['last_activity'] = $xprofile->last_activity;
61 + }
62 +
63 + if ($canViewProfile) {
64 + $profile['website'] = Arr::get($xprofile->meta, 'website');
65 + $profile['created_at'] = $xprofile->created_at->format('Y-m-d H:i:s');
66 + $profile['social_links'] = (object) Arr::get($xprofile->meta, 'social_links', []);
67 + $profile['compilation_score'] = $xprofile->getCompletionScore();
68 + $profile['short_description_rendered'] = wp_kses_post(FeedsHelper::mdToHtml($xprofile->short_description));
69 + }
70 +
71 + $currentUserId = get_current_user_id();
72 +
73 + $isOwn = $xprofile->user_id == $currentUserId;
74 +
75 + $isAdmin = Helper::isSiteAdmin($currentUserId);
76 +
77 + if ($isOwn || $isAdmin) {
78 + $enableUserSync = Utility::getPrivacySetting('enable_user_sync') === 'yes';
79 + $nameArray = explode(' ', trim((string) $xprofile->display_name));
80 + $xprofileFirstName = array_shift($nameArray);
81 + $xprofileLastName = implode(' ', $nameArray);
82 +
49 83 $profile['email'] = $user->user_email;
50 - $profile['first_name'] = $user->first_name;
51 - $profile['last_name'] = $user->last_name;
84 + $profile['first_name'] = $enableUserSync ? $user->first_name : $xprofileFirstName;
85 + $profile['last_name'] = $enableUserSync ? $user->last_name : $xprofileLastName;
52 86 $profile['short_description'] = $xprofile->short_description;
53 87 $profile['can_change_username'] = $isAdmin || Utility::getPrivacySetting('can_customize_username') === 'yes';
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';
54 90 }
55 91
92 + $profileBaseUrl = Helper::baseUrl('u/' . $xprofile->username . '/');
93 +
94 + $profile['profile_navs'] = [
95 + [
96 + 'slug' => 'user_profile',
97 + 'title' => __('About', 'fluent-community'),
98 + 'url' => $profileBaseUrl,
99 + 'wrapper_class' => 'fcom_profile_about',
100 + 'route' => [
101 + 'name' => 'user_profile'
102 + ]
103 + ],
104 + [
105 + 'slug' => 'user_profile_feeds',
106 + 'title' => __('Posts', 'fluent-community'),
107 + 'wrapper_class' => 'fcom_profile_posts',
108 + 'url' => $profileBaseUrl . 'posts',
109 + 'route' => [
110 + 'name' => 'user_profile_feeds'
111 + ]
112 + ]
113 + ];
114 +
115 + if ($profile['canViewUserSpaces']) {
116 + $profile['profile_navs'][] = [
117 + 'slug' => 'user_spaces',
118 + 'wrapper_class' => 'fcom_profile_spaces',
119 + 'title' => __('Spaces', 'fluent-community'),
120 + 'url' => $profileBaseUrl . 'spaces',
121 + 'route' => [
122 + 'name' => 'user_spaces'
123 + ]
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 + }
137 + }
138 +
139 + $profile['profile_navs'][] = [
140 + 'slug' => 'user_comments',
141 + 'wrapper_class' => 'fcom_profile_comments',
142 + 'title' => __('Comments', 'fluent-community'),
143 + 'url' => $profileBaseUrl . 'comments',
144 + 'route' => [
145 + 'name' => 'user_comments'
146 + ]
147 + ];
148 +
149 + $profile['profile_nav_actions'] = [];
150 +
151 + $profile = apply_filters('fluent_community/profile_view_data', $profile, $xprofile, $isAdmin);
152 +
56 153 return [
57 154 'profile' => $profile
58 155 ];
59 156 }
@@ -59,12 +156,31 @@
59 156 }
60 157
61 158 public function patchProfile(Request $request, $userName)
62 159 {
63 - $xprofile = $this->verfifyAndGetProfile($userName);
160 + $xprofile = $this->verifyAndGetProfile($userName);
64 161
65 - $updateData = $request->get('data');
162 + $updateData = $request->get('data', []);
66 163
164 + if (!empty($updateData['status']) && $updateData['status'] === 'deactivated' && $xprofile->status === 'active') {
165 + // handle deactivation
166 + $canDeactivate = Utility::getPrivacySetting('can_deactive_account') === 'yes' || Helper::isSiteAdmin();
167 + if (!$canDeactivate) {
168 + return $this->sendError([
169 + 'message' => __('You are not allowed to deactivate this account.', 'fluent-community')
170 + ]);
171 + }
172 +
173 + $xprofile->status = '';
174 + $xprofile->save();
175 + update_user_meta($xprofile->user_id, '_fcom_deactivated_at', current_time('mysql'));
176 + do_action('fluent_community/profile_deactivated', $xprofile);
177 +
178 + return [
179 + 'message' => __('Your profile has been deactivated successfully.', 'fluent-community')
180 + ];
181 + }
182 +
67 183 $mediaTypes = ['cover_photo', 'avatar'];
68 184
69 185 foreach ($mediaTypes as $type) {
70 186 if (!empty($updateData[$type])) {
@@ -70,9 +186,9 @@
70 186 if (!empty($updateData[$type])) {
71 187 $media = Helper::getMediaFromUrl($updateData[$type]);
72 188 if (!$media || $media->is_active) {
73 189 return $this->sendError([
74 - 'message' => 'Invalid media image. Please upload a new one.'
190 + 'message' => __('Invalid media image. Please upload a new one.', 'fluent-community')
75 191 ]);
76 192 }
77 193
78 194 $updateData[$type] = $media->public_url;
@@ -86,11 +202,13 @@
86 202 }
87 203
88 204 $deletedMedias = [];
89 205
90 - if (!empty($updateData['avatar'])) {
206 + if (isset($updateData['avatar'])) {
91 207
92 - $deletedMedias[] = $xprofile->avatar;
208 + if ($xprofile->hasCustomAvatar()) {
209 + $deletedMedias[] = Arr::get($xprofile->getAttributes(), 'avatar');
210 + }
93 211
94 212 $xprofile->avatar = $updateData['avatar'];
95 213
96 214 if (defined('FLUENTCRM')) {
@@ -97,13 +215,16 @@
97 215 $contact = $xprofile->contact;
98 216
99 217 if ($contact) {
100 218 $contact->update([
101 - 'avatar' => $updateData['avatar']
219 + 'avatar' => $updateData['avatar'] ?: null
102 220 ]);
103 221 }
104 222 }
105 223
224 + if (empty($updateData['avatar'])) {
225 + Utility::forgetCache('user_avatar_' . $xprofile->user_id);
226 + }
106 227 }
107 228
108 229 if (isset($updateData['cover_photo'])) {
109 230 $deletedMedias[] = Arr::get($xprofile->meta, 'cover_photo');
@@ -128,15 +249,15 @@
128 249 {
129 250 $currentUser = $this->getUser(true);
130 251 $data = $request->get('data', []);
131 252
132 - if ($currentUser->isCommunityModerator()) {
133 - $xProfile = XProfile::where('user_id', $data['user_id'])->firstOrFail();
134 - } else {
135 - $xProfile = XProfile::where('username', $userName)->firstOrFail();
136 - if ($xProfile->user_id != get_current_user_id()) {
253 + /** @var XProfile $xProfile */
254 + $xProfile = XProfile::where('username', $userName)->firstOrFail();
255 +
256 + if ($xProfile->user_id != get_current_user_id()) {
257 + if(!$currentUser->isCommunityModerator()) {
137 258 return $this->sendError([
138 - 'message' => 'You are not allowed to update this profile'
259 + 'message' => __('You are not allowed to update this profile', 'fluent-community')
139 260 ]);
140 261 }
141 262 }
142 263
@@ -147,9 +268,10 @@
147 268 ]);
148 269
149 270 $updateData = Arr::only($data, ['first_name', 'last_name', 'short_description', 'website']);
150 271
151 - $currentUser = User::findOrFail(get_current_user_id());
272 + $updateData = apply_filters('fluent_community/update_profile_data', $updateData, $data, $xProfile, $currentUser);
273 +
152 274 $meta = $xProfile->meta;
153 275
154 276 $userNameChanged = false;
155 277
@@ -190,10 +312,13 @@
190 312 $userNameChanged = $userName != $xProfile->username;
191 313 }
192 314
193 315 if (Helper::isFeatureEnabled('user_badge')) {
194 - $badgeSlug = Arr::get($data, 'badge_slug');
195 - $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)));
196 321 }
197 322 } else if (Utility::getPrivacySetting('can_customize_username')) {
198 323 $userName = Arr::get($data, 'username');
199 324
@@ -209,10 +334,10 @@
209 334 return $this->sendError([
210 335 'message' => __('Community Username already taken by someone else', 'fluent-community')
211 336 ]);
212 337 }
213 -
214 - if(strlen($userName) < 3) {
338 +
339 + if (strlen($userName) < 3) {
215 340 return $this->sendError([
216 341 'message' => __('Username should be at least 3 characters long.', 'fluent-community')
217 342 ]);
218 343 }
@@ -226,27 +351,43 @@
226 351
227 352 $updateData['username'] = $userName;
228 353 $userNameChanged = true;
229 354 }
230 -
231 355 }
232 356
233 357 $updateData['display_name'] = trim(sanitize_text_field(Arr::get($data, 'first_name') . ' ' . Arr::get($data, 'last_name')));
234 - $updateData['short_description'] = CustomSanitizer::unslashMarkdown(sanitize_textarea_field(trim(Arr::get($data, 'short_description'))));
235 - $meta['website'] = sanitize_url(Arr::get($data, 'website'));
358 +
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', '')));
236 362 $socialLinks = Arr::get($data, 'social_links', []);
237 363
238 364 $maxDescriptionLength = apply_filters('fluent_community/max_profile_description_length', 5000);
239 365 if ($updateData['short_description'] && strlen($updateData['short_description']) > $maxDescriptionLength) {
240 366 return $this->sendError([
241 - 'message' => sprintf(__('Profile bio should not exceed %d characters.', 'fluent-community'), $maxDescriptionLength)
367 + 'message' => sprintf(
368 + /* translators: %d: Maximum number of characters allowed in the profile bio. */
369 + __('Profile bio should not exceed %d characters.', 'fluent-community'),
370 + $maxDescriptionLength
371 + )
242 372 ]);
243 373 }
244 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 +
245 386 if ($socialLinks) {
246 387 $socialLinks = array_filter($socialLinks);
247 388 $formattedSocialLinkes = [];
248 - $socialLinkProviders = ProfileHelper::socialLinkProviders();
389 + $socialLinkProviders = ProfileHelper::socialLinkProviders(true);
249 390 foreach ($socialLinks as $linkName => $socialLink) {
250 391 if (isset($socialLinkProviders[$linkName])) {
251 392 $formattedSocialLinkes[$linkName] = sanitize_text_field(trim($socialLink));
252 393 }
@@ -253,14 +394,15 @@
253 394 }
254 395 $meta['social_links'] = $formattedSocialLinkes;
255 396 }
256 397
398 + $meta['short_description_rendered'] = wp_kses_post(FeedsHelper::mdToHtml($updateData['short_description']));
399 +
257 400 $updateData['meta'] = $meta;
258 401
259 402 $xProfile->fill($updateData);
260 403 $xProfile->save();
261 404
262 -
263 405 // Let's update the user's details
264 406 $xProfile->user->updateCustomData($updateData);
265 407 $xProfile->compilation_score = $xProfile->getCompletionScore();
266 408
@@ -271,8 +413,39 @@
271 413 'redirect_url' => Helper::baseUrl('u/' . $xProfile->username . '/update')
272 414 ];
273 415 }
274 416
417 + $isOwn = $xProfile->user_id == get_current_user_id();
418 + $canEditUsers = current_user_can('edit_users');
419 + if ($canEditUsers || (Utility::getPrivacySetting('can_change_email') === 'yes' && $isOwn)) {
420 + $emailAddress = Arr::get($data, 'email');
421 +
422 + if ($emailAddress && is_email($emailAddress) && $emailAddress != $xProfile->user->user_email) {
423 + $owner_id = email_exists($emailAddress);
424 + if ($owner_id && $owner_id != $xProfile->user_id) {
425 + return $this->sendError([
426 + 'message' => __('Email address already taken by someone else. Please use a different email address.', 'fluent-community')
427 + ]);
428 + }
429 +
430 + // Let's check if it's their own
431 + $requireVerification = $isOwn && !$canEditUsers;
432 + if ($requireVerification) {
433 + $currentUser = get_user_by('ID', $xProfile->user_id);
434 + ProfileHelper::sendConfirmationOnProfileEmailChange($currentUser, $emailAddress);
435 + return [
436 + 'message' => __('Email address change is pending. Please check your inbox to verify the new email address.', 'fluent-community'),
437 + 'profile' => $xProfile
438 + ];
439 + }
440 +
441 + wp_update_user([
442 + 'user_email' => $emailAddress,
443 + 'ID' => $xProfile->user_id
444 + ]);
445 + }
446 + }
447 +
275 448 return [
276 449 'message' => __('Profile has been updated', 'fluent-community'),
277 450 'profile' => $xProfile
278 451 ];
@@ -277,10 +450,130 @@
277 450 'profile' => $xProfile
278 451 ];
279 452 }
280 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 +
543 + public function getAllMemberships(Request $request, $userName)
544 + {
545 + /** @var XProfile $xProfile */
546 + $xProfile = XProfile::where('username', $userName)->firstOrFail();
547 +
548 + $currentUser = $this->getUser();
549 +
550 + if (!ProfileHelper::canViewUserSpaces($xProfile->user_id, $currentUser)) {
551 + return $this->sendError([
552 + 'message' => __('You are not allowed to view this profile\'s membership.', 'fluent-community'),
553 + 'permission_failed' => true
554 + ]);
555 + }
556 +
557 + $canSeeSecret = $xProfile->user_id == get_current_user_id()
558 + || ($currentUser && $currentUser->isCommunityModerator());
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 +
568 + return apply_filters('fluent_community/profile_all_memberships_api_response', [
569 + 'memberships' => $memberships
570 + ], $request->all());
571 + }
572 +
281 573 public function getSpaces(Request $request, $userName)
282 574 {
575 + /** @var XProfile $xProfile */
283 576 $xProfile = XProfile::where('username', $userName)->firstOrFail();
284 577 $currentUser = $this->getUser();
285 578
286 579 if (!ProfileHelper::canViewUserSpaces($xProfile->user_id, $currentUser)) {
@@ -301,14 +594,79 @@
301 594 ->get();
302 595 }
303 596
304 597 foreach ($spaces as $space) {
598 + $shouldHideMembersCount = Arr::get($space->settings, 'hide_members_count') == 'yes';
599 + $canViewMembers = $currentUser && $space->verifyUserPermisson($currentUser, 'can_view_members', false);
600 + if ($shouldHideMembersCount && !$canViewMembers) {
601 + $space->members_count = 0;
602 + continue;
603 + }
305 604 $space->members_count = $space->members()->count();
306 605 }
307 606
308 - return [
309 - 'spaces' => $spaces
607 + $data = [
608 + 'spaces' => $spaces
609 + ];
610 +
611 + return apply_filters('fluent_community/profile_spaces_api_response', $data, $request->all());
612 + }
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
310 666 ];
667 +
668 + return apply_filters('fluent_community/profile_courses_api_response', $data, $request->all());
311 669 }
312 670
313 671 public function getComments(Request $request, $userName)
314 672 {
@@ -315,9 +673,9 @@
315 673 $xProfile = XProfile::where('username', $userName)->first();
316 674
317 675 if (!$xProfile) {
318 676 return $this->sendError([
319 - 'message' => 'Profile not found'
677 + 'message' => __('Profile not found', 'fluent-community')
320 678 ]);
321 679 }
322 680
323 681 $currentUser = $this->getUser();
@@ -323,20 +681,18 @@
323 681 $currentUser = $this->getUser();
324 682 $hasAllAccess = $xProfile->user_id == get_current_user_id() || ($currentUser && $currentUser->isCommunityModerator());
325 683
326 684 $comments = Comment::where('user_id', $xProfile->user_id)
685 + ->where('status', 'published')
327 686 ->with([
328 - 'post' => function ($q) {
329 - $q->select(['id', 'title', 'message', 'type', 'space_id', 'slug', 'created_at'])
330 - ->with([
331 - 'space' => function ($q) {
332 - $q->select(['id', 'title', 'slug', 'type']);
333 - }
334 - ]);
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));
335 691 }
336 692 ])
337 - ->when(!$hasAllAccess, function ($q) use ($xProfile) {
338 - $q->whereHas('post', function ($query) use ($xProfile) {
693 + ->when(!$hasAllAccess, function ($q) {
694 + $q->whereHas('post', function ($query) {
339 695 $query->byUserAccess(get_current_user_id());
340 696 $query->where('type', 'text');
341 697 });
342 698 })
@@ -342,12 +698,24 @@
342 698 })
343 699 ->orderBy('id', 'desc')
344 700 ->paginate();
345 701
346 - return [
347 - 'comments' => $comments,
348 - 'xprofile' => $xProfile
349 - ];
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 +
712 + $data = [
713 + 'comments' => $comments,
714 + 'xprofile' => $xProfile
715 + ];
716 +
717 + return apply_filters('fluent_community/profile_comments_api_response', $data, $request->all());
350 718 }
351 719
352 720 public function getNotificationPreferance(Request $request, $userName)
353 721 {
@@ -352,39 +720,75 @@
352 720 public function getNotificationPreferance(Request $request, $userName)
353 721 {
354 722 $emailPref = Utility::getEmailNotificationSettings();
355 723
356 - $xProfile = $this->verfifyAndGetProfile($userName);
724 + $xProfile = $this->verifyAndGetProfile($userName);
357 725
358 726 $globalPreferances = NotificationPref::getGlobalPrefs();
359 - $userPrefs = NotificationSubscription::where('user_id', $xProfile->user_id)
360 - ->select(['notification_type', 'is_read', 'object_id'])
361 - ->get();
362 727
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);
732 +
733 + $frequencyMaps = [
734 + 0 => 'disabled',
735 + 1 => 'hourly',
736 + 2 => 'daily',
737 + 3 => 'weekly'
738 + ];
739 +
363 740 $userGlobalPrefs = [];
364 741 $spaceWisePrefs = [];
365 - foreach ($userPrefs as $pref) {
366 - if (!$pref->object_id) {
367 - $userGlobalPrefs[$pref->notification_type] = $pref->is_read ? 'yes' : 'no';
368 - } else {
369 - if (empty($spaceWisePrefs[$pref->object_id])) {
370 - $spaceWisePrefs[$pref->object_id] = [];
742 + foreach ($userPrefs as $prefKey => $prefValue) {
743 + if ($prefKey === 'message_email_frequency') {
744 + $userGlobalPrefs[$prefKey] = isset($frequencyMaps[$prefValue]) ? $frequencyMaps[$prefValue] : 'default';
745 + continue;
746 + }
747 +
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] = [];
371 753 }
372 - $spaceWisePrefs[$pref->object_id][$pref->notification_type] = $pref->is_read;
754 +
755 + $spaceWisePrefs[$spaceId][$matches[1]] = $prefValue;
756 + continue;
373 757 }
758 +
759 + $userGlobalPrefs[$prefKey] = $prefValue ? 'yes' : 'no';
374 760 }
375 761
762 + $messagingConfig = Utility::getOption('_messaging_settings', []);
763 + $isGlobalPerUser = Arr::get($messagingConfig, 'messaging_email_frequency') == 'disabled';
764 +
765 + $pushAvailable = PushNotificationModule::isAvailable();
766 +
376 767 $userGlobalPrefsDefaults = [
377 - 'digest_mail' => Arr::get($globalPreferances, 'digest_email_status') ? 'yes' : 'no',
378 - 'mention_mail' => Arr::get($globalPreferances, 'mention_mail') ? 'yes' : 'no',
379 - 'reply_my_com_mail' => Arr::get($globalPreferances, 'reply_my_com_mail') ? 'yes' : 'no',
380 - 'com_my_post_mail' => Arr::get($globalPreferances, 'com_my_post_mail') ? 'yes' : 'no',
768 + 'digest_mail' => Arr::get($globalPreferances, 'digest_email_status') ? 'yes' : 'no',
769 + 'mention_mail' => Arr::get($globalPreferances, 'mention_mail') ? 'yes' : 'no',
770 + 'reply_my_com_mail' => Arr::get($globalPreferances, 'reply_my_com_mail') ? 'yes' : 'no',
771 + 'com_my_post_mail' => Arr::get($globalPreferances, 'com_my_post_mail') ? 'yes' : 'no',
772 + 'message_email_frequency' => $isGlobalPerUser ? 'disabled' : 'default'
381 773 ];
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 +
382 784 $userGlobalPrefs = wp_parse_args($userGlobalPrefs, $userGlobalPrefsDefaults);
383 785
384 - $spaceGroups = SpaceGroup::with(['spaces' => function ($query) {
385 - $query->whereHas('members', function ($q) {
386 - $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)
790 + ->where('status', 'active');
387 791 })
388 792 ->where('type', 'community');
389 793 }])
390 794 ->orderBy('serial', 'ASC')
@@ -394,9 +798,8 @@
394 798 foreach ($spaceGroups as $group) {
395 799 if ($group->spaces->isEmpty()) {
396 800 continue;
397 801 }
398 -
399 802 $formattedSpaces = [];
400 803 foreach ($group->spaces as $space) {
401 804
402 805 $pref = '';
@@ -415,9 +818,8 @@
415 818 'icon' => $space->getIconMark(),
416 819 'pref' => $pref
417 820 ];
418 821 }
419 -
420 822 if ($formattedSpaces) {
421 823 $formattedSpaceGroups[] = [
422 824 'id' => $group->id,
423 825 'title' => $group->title,
@@ -425,8 +827,44 @@
425 827 ];
426 828 }
427 829 }
428 830
831 + // let's find the other spaces
832 + $otherSpaces = Space::whereHas('members', function ($q) use ($xProfile) {
833 + $q->where('user_id', $xProfile->user_id);
834 + })
835 + ->whereNull('parent_id')
836 + ->orderBy('title', 'ASC')
837 + ->get();
838 +
839 + if (!$otherSpaces->isEmpty()) {
840 + $formattedSpaces = [];
841 + foreach ($otherSpaces as $space) {
842 + $pref = '';
843 + if (isset($spaceWisePrefs[$space->id])) {
844 + $perfs = (array)$spaceWisePrefs[$space->id];
845 + if (!empty($perfs['np_by_member_mail'])) {
846 + $pref = 'all_member_posts';
847 + } else if (!empty($perfs['np_by_admin_mail'])) {
848 + $pref = 'admin_only_posts';
849 + }
850 + }
851 +
852 + $formattedSpaces[] = [
853 + 'id' => $space->id,
854 + 'title' => $space->title,
855 + 'icon' => $space->getIconMark(),
856 + 'pref' => $pref
857 + ];
858 + }
859 +
860 + $formattedSpaceGroups[] = [
861 + 'id' => 'other_space_group',
862 + 'title' => __('Other Spaces', 'fluent-community'),
863 + 'spaces' => $formattedSpaces
864 + ];
865 + }
866 +
429 867 $digestDay = (string)Arr::get($emailPref, 'digest_mail_day', 'tue');
430 868 if ($digestDay) {
431 869 $maps = [
432 870 'mon' => __('Monday', 'fluent-community'),
@@ -441,27 +879,54 @@
441 879 $digestDay = $maps[$digestDay];
442 880 }
443 881 }
444 882
445 - return [
446 - 'user_globals' => (object)$userGlobalPrefs,
447 - 'spaceGroups' => $formattedSpaceGroups,
448 - 'space_prefs' => $spaceWisePrefs,
449 - 'digestEmailDay' => $digestDay
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 +
891 + $data = [
892 + 'user_globals' => (object)$userGlobalPrefs,
893 + 'spaceGroups' => $formattedSpaceGroups,
894 + 'space_prefs' => $spaceWisePrefs,
895 + 'digestEmailDay' => $digestDay,
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,
450 899 ];
900 +
901 + return apply_filters('fluent_community/profile_notification_pref_api_response', $data, $request->all());
451 902 }
452 903
453 904 public function saveNotificationPreferance(Request $request, $userName)
454 905 {
455 - $xProfile = $this->verfifyAndGetProfile($userName);
906 + $xProfile = $this->verifyAndGetOwnProfile($userName);
456 907
457 908 $userPrefs = $request->get('user_globals', []);
458 909 $sapcePrefs = $request->get('space_prefs', []);
459 910
911 + $messagingPref = Arr::get($userPrefs, 'message_email_frequency');
912 +
460 913 $userPrefs = array_map(function ($item) {
461 914 return $item == 'yes' ? 1 : 0;
462 915 }, $userPrefs);
463 916
917 + if ($messagingPref == 'hourly') {
918 + $userPrefs['message_email_frequency'] = 1;
919 + } else if ($messagingPref == 'daily') {
920 + $userPrefs['message_email_frequency'] = 2;
921 + } else if ($messagingPref == 'disabled') {
922 + $userPrefs['message_email_frequency'] = 0;
923 + } else if ($messagingPref == 'weekly') {
924 + $userPrefs['message_email_frequency'] = 3;
925 + } else {
926 + unset($userPrefs['message_email_frequency']);
927 + }
928 +
464 929 foreach ($sapcePrefs as $spaceId => $pref) {
465 930 $spaceId = (int)$spaceId;
466 931 if (!$pref || !$spaceId) {
467 932 continue;
@@ -474,9 +939,8 @@
474 939 $userPrefs['np_by_admin_mail_' . $spaceId] = 1;
475 940 }
476 941 }
477 942
478 -
479 943 NotificationPref::updateUserPrefs($xProfile->user_id, $userPrefs);
480 944
481 945 return [
482 946 'prefs' => $userPrefs,
@@ -483,15 +947,75 @@
483 947 'message' => __('Email Notification preferences have been updated', 'fluent-community')
484 948 ];
485 949 }
486 950
487 - private function verfifyAndGetProfile($userName)
951 + public function reconfirmEmail(Request $request, $userName)
488 952 {
953 + if (!defined('FLUENTCRM')) {
954 + return $this->sendError([
955 + 'message' => __('FluentCRM is not available on this site', 'fluent-community')
956 + ]);
957 + }
958 +
489 959 $xProfile = XProfile::where('username', $userName)->firstOrFail();
490 960
491 - $currentUser = $this->getUser();
492 - 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)) {
493 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'));
494 1018 }
495 1019
496 1020 return $xProfile;
497 1021 }