PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.10.01
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.10.01
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 +692 -97 1.0.932.10.01 View file →
@@ -1,13 +1,14 @@
1 1 <?php
2 2
3 3 namespace FluentCommunity\App\Http\Controllers;
4 4
5 +use FluentCommunity\App\Functions\Utility;
5 6 use FluentCommunity\App\Models\Comment;
6 -use FluentCommunity\App\Models\NotificationSubscription;
7 +use FluentCommunity\App\Models\Feed;
7 8 use FluentCommunity\App\Models\Space;
8 9 use FluentCommunity\App\Models\SpaceGroup;
9 -use FluentCommunity\App\Models\User;
10 +use FluentCommunity\App\Models\SpaceUserPivot;
10 11 use FluentCommunity\App\Models\XProfile;
11 12 use FluentCommunity\App\Services\CustomSanitizer;
12 13 use FluentCommunity\App\Services\FeedsHelper;
13 14 use FluentCommunity\App\Services\Helper;
@@ -14,42 +15,142 @@
14 15 use FluentCommunity\App\Services\NotificationPref;
15 16 use FluentCommunity\App\Services\ProfileHelper;
16 17 use FluentCommunity\Framework\Http\Request\Request;
17 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;
18 24
19 25 class ProfileController extends Controller
20 26 {
21 27 public function getProfile(Request $request, $userName)
22 28 {
23 - $xprofile = XProfile::where('username', $userName)->firstOrFail();
29 + /** @var XProfile $xprofile */
30 + $xprofile = XProfile::where('username', $userName)
31 + ->firstOrFail();
24 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 +
25 41 $user = get_user_by('ID', $xprofile->user_id);
26 42
27 43 $profile = [
28 - 'user_id' => $xprofile->user_id,
29 - 'is_verified' => $xprofile->is_verified,
30 - 'display_name' => $xprofile->display_name,
31 - 'username' => $xprofile->username,
32 - 'avatar' => $xprofile->avatar,
33 - 'created_at' => $xprofile->created_at->format('Y-m-d H:i:s'),
34 - 'last_activity' => $xprofile->last_activity,
35 - 'short_description_rendered' => FeedsHelper::mdToHtml($xprofile->short_description),
36 - 'cover_photo' => Arr::get($xprofile->meta, 'cover_photo'),
37 - 'website' => Arr::get($xprofile->meta, 'website'),
38 - 'social_links' => (object)Arr::get($xprofile->meta, 'social_links', []),
39 - 'status' => $xprofile->status,
40 - 'badge_slug' => Arr::get($xprofile->meta, 'badge_slug'),
41 - 'compilation_score' => $xprofile->getCompletionScore(),
42 - 'total_points' => $xprofile->total_points
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())
43 57 ];
44 58
45 - if ($xprofile->user_id == get_current_user_id() || current_user_can('edit_users')) {
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 +
46 83 $profile['email'] = $user->user_email;
47 - $profile['first_name'] = $user->first_name;
48 - $profile['last_name'] = $user->last_name;
84 + $profile['first_name'] = $enableUserSync ? $user->first_name : $xprofileFirstName;
85 + $profile['last_name'] = $enableUserSync ? $user->last_name : $xprofileLastName;
49 86 $profile['short_description'] = $xprofile->short_description;
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';
50 90 }
51 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 +
52 153 return [
53 154 'profile' => $profile
54 155 ];
55 156 }
@@ -55,12 +156,31 @@
55 156 }
56 157
57 158 public function patchProfile(Request $request, $userName)
58 159 {
59 - $xprofile = $this->verfifyAndGetProfile($userName);
160 + $xprofile = $this->verifyAndGetProfile($userName);
60 161
61 - $updateData = $request->get('data');
162 + $updateData = $request->get('data', []);
62 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 +
63 183 $mediaTypes = ['cover_photo', 'avatar'];
64 184
65 185 foreach ($mediaTypes as $type) {
66 186 if (!empty($updateData[$type])) {
@@ -66,9 +186,9 @@
66 186 if (!empty($updateData[$type])) {
67 187 $media = Helper::getMediaFromUrl($updateData[$type]);
68 188 if (!$media || $media->is_active) {
69 189 return $this->sendError([
70 - 'message' => 'Invalid media image. Please upload a new one.'
190 + 'message' => __('Invalid media image. Please upload a new one.', 'fluent-community')
71 191 ]);
72 192 }
73 193
74 194 $updateData[$type] = $media->public_url;
@@ -82,11 +202,13 @@
82 202 }
83 203
84 204 $deletedMedias = [];
85 205
86 - if (!empty($updateData['avatar'])) {
206 + if (isset($updateData['avatar'])) {
87 207
88 - $deletedMedias[] = $xprofile->avatar;
208 + if ($xprofile->hasCustomAvatar()) {
209 + $deletedMedias[] = Arr::get($xprofile->getAttributes(), 'avatar');
210 + }
89 211
90 212 $xprofile->avatar = $updateData['avatar'];
91 213
92 214 if (defined('FLUENTCRM')) {
@@ -93,16 +215,19 @@
93 215 $contact = $xprofile->contact;
94 216
95 217 if ($contact) {
96 218 $contact->update([
97 - 'avatar' => $updateData['avatar']
219 + 'avatar' => $updateData['avatar'] ?: null
98 220 ]);
99 221 }
100 222 }
101 223
224 + if (empty($updateData['avatar'])) {
225 + Utility::forgetCache('user_avatar_' . $xprofile->user_id);
226 + }
102 227 }
103 228
104 - if (!empty($updateData['cover_photo'])) {
229 + if (isset($updateData['cover_photo'])) {
105 230 $deletedMedias[] = Arr::get($xprofile->meta, 'cover_photo');
106 231 $xprofile->meta = wp_parse_args(['cover_photo' => $updateData['cover_photo']], $xprofile->meta);
107 232 }
108 233
@@ -124,15 +249,15 @@
124 249 {
125 250 $currentUser = $this->getUser(true);
126 251 $data = $request->get('data', []);
127 252
128 - if ($currentUser->isCommunityModerator()) {
129 - $xProfile = XProfile::where('user_id', $data['user_id'])->firstOrFail();
130 - } else {
131 - $xProfile = XProfile::where('username', $userName)->firstOrFail();
132 - 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()) {
133 258 return $this->sendError([
134 - 'message' => 'You are not allowed to update this profile'
259 + 'message' => __('You are not allowed to update this profile', 'fluent-community')
135 260 ]);
136 261 }
137 262 }
138 263
@@ -137,17 +262,16 @@
137 262 }
138 263
139 264 $this->validate($data, [
140 265 'first_name' => 'required',
141 - 'last_name' => 'required',
142 266 ], [
143 - 'first_name.required' => __('First name is required', 'fluent-community'),
144 - 'last_name.required' => __('Last name is required', 'fluent-community'),
267 + 'first_name.required' => __('First name is required', 'fluent-community')
145 268 ]);
146 269
147 270 $updateData = Arr::only($data, ['first_name', 'last_name', 'short_description', 'website']);
148 271
149 - $currentUser = User::findOrFail(get_current_user_id());
272 + $updateData = apply_filters('fluent_community/update_profile_data', $updateData, $data, $xProfile, $currentUser);
273 +
150 274 $meta = $xProfile->meta;
151 275
152 276 $userNameChanged = false;
153 277
@@ -165,43 +289,105 @@
165 289 $userName = CustomSanitizer::sanitizeUserName($userName);
166 290
167 291 if (!$userName) {
168 292 return $this->sendError([
169 - 'message' => __('Invalid username. Only latin chars with _ & - is allowed', 'fluent-community')
293 + 'message' => __('Invalid username. Only Latin characters with _ & - are allowed.', 'fluent-community')
170 294 ]);
171 295 }
172 296
173 297 if (XProfile::where('username', $userName)->where('user_id', '!=', $xProfile->user_id)->exists()) {
174 298 return $this->sendError([
175 - 'message' => 'Space Username already taken by someone else'
299 + 'message' => __('Community Username already taken by someone else', 'fluent-community')
176 300 ]);
177 301 }
302 +
303 + $userExist = get_user_by('user_login', $userName);
304 +
305 + if ($userExist && $userExist->ID != $xProfile->user_id) {
306 + return $this->sendError([
307 + 'message' => __('Username already taken by someone else. Please use a different username.', 'fluent-community')
308 + ]);
309 + }
310 +
178 311 $updateData['username'] = $userName;
179 312 $userNameChanged = $userName != $xProfile->username;
180 313 }
181 314
182 315 if (Helper::isFeatureEnabled('user_badge')) {
183 - $badgeSlug = Arr::get($data, 'badge_slug');
184 - $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)));
185 321 }
322 + } else if (Utility::getPrivacySetting('can_customize_username')) {
323 + $userName = Arr::get($data, 'username');
324 +
325 + if ($xProfile->username != $userName) {
326 + $userName = strtolower(CustomSanitizer::sanitizeUserName($userName));
327 + if (!$userName) {
328 + return $this->sendError([
329 + 'message' => __('Invalid username. Only Latin characters with _ & - are allowed.', 'fluent-community')
330 + ]);
331 + }
332 +
333 + if (XProfile::where('username', $userName)->where('user_id', '!=', $xProfile->user_id)->exists()) {
334 + return $this->sendError([
335 + 'message' => __('Community Username already taken by someone else', 'fluent-community')
336 + ]);
337 + }
338 +
339 + if (strlen($userName) < 3) {
340 + return $this->sendError([
341 + 'message' => __('Username should be at least 3 characters long.', 'fluent-community')
342 + ]);
343 + }
344 +
345 + $reservedUserNames = ProfileHelper::getReservedUserNames();
346 + if (in_array($userName, $reservedUserNames)) {
347 + return $this->sendError([
348 + 'message' => __('Please use another username. This username is reserved', 'fluent-community')
349 + ]);
350 + }
351 +
352 + $updateData['username'] = $userName;
353 + $userNameChanged = true;
354 + }
186 355 }
187 356
188 357 $updateData['display_name'] = trim(sanitize_text_field(Arr::get($data, 'first_name') . ' ' . Arr::get($data, 'last_name')));
189 - $updateData['short_description'] = CustomSanitizer::unslashMarkdown(sanitize_textarea_field(trim(Arr::get($data, 'short_description'))));
190 - $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', '')));
191 362 $socialLinks = Arr::get($data, 'social_links', []);
192 363
193 364 $maxDescriptionLength = apply_filters('fluent_community/max_profile_description_length', 5000);
194 365 if ($updateData['short_description'] && strlen($updateData['short_description']) > $maxDescriptionLength) {
195 366 return $this->sendError([
196 - 'message' => sprintf(__('Profile Bio should not be more than %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 + )
197 372 ]);
198 373 }
199 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 +
200 386 if ($socialLinks) {
201 387 $socialLinks = array_filter($socialLinks);
202 388 $formattedSocialLinkes = [];
203 - $socialLinkProviders = ProfileHelper::socialLinkProviders();
389 + $socialLinkProviders = ProfileHelper::socialLinkProviders(true);
204 390 foreach ($socialLinks as $linkName => $socialLink) {
205 391 if (isset($socialLinkProviders[$linkName])) {
206 392 $formattedSocialLinkes[$linkName] = sanitize_text_field(trim($socialLink));
207 393 }
@@ -208,19 +394,19 @@
208 394 }
209 395 $meta['social_links'] = $formattedSocialLinkes;
210 396 }
211 397
398 + $meta['short_description_rendered'] = wp_kses_post(FeedsHelper::mdToHtml($updateData['short_description']));
399 +
212 400 $updateData['meta'] = $meta;
213 401
214 402 $xProfile->fill($updateData);
215 403 $xProfile->save();
216 404
217 -
218 405 // Let's update the user's details
219 406 $xProfile->user->updateCustomData($updateData);
220 407 $xProfile->compilation_score = $xProfile->getCompletionScore();
221 408
222 -
223 409 if ($userNameChanged) {
224 410 return [
225 411 'message' => __('Profile has been updated', 'fluent-community'),
226 412 'profile' => $xProfile,
@@ -227,8 +413,39 @@
227 413 'redirect_url' => Helper::baseUrl('u/' . $xProfile->username . '/update')
228 414 ];
229 415 }
230 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 +
231 448 return [
232 449 'message' => __('Profile has been updated', 'fluent-community'),
233 450 'profile' => $xProfile
234 451 ];
@@ -233,12 +450,140 @@
233 450 'profile' => $xProfile
234 451 ];
235 452 }
236 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 +
237 573 public function getSpaces(Request $request, $userName)
238 574 {
575 + /** @var XProfile $xProfile */
239 576 $xProfile = XProfile::where('username', $userName)->firstOrFail();
240 577 $currentUser = $this->getUser();
578 +
579 + if (!ProfileHelper::canViewUserSpaces($xProfile->user_id, $currentUser)) {
580 + return $this->sendError([
581 + 'message' => __('You are not allowed to view this profile\'s spaces.', 'fluent-community'),
582 + 'permission_failed' => true
583 + ]);
584 + }
585 +
241 586 if ($xProfile->user_id == get_current_user_id() || ($currentUser && $currentUser->isCommunityModerator())) {
242 587 $spaces = $xProfile->spaces()
243 588 ->wherePivot('status', 'active')
244 589 ->get();
@@ -249,14 +594,79 @@
249 594 ->get();
250 595 }
251 596
252 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 + }
253 604 $space->members_count = $space->members()->count();
254 605 }
255 606
256 - return [
257 - '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
258 666 ];
667 +
668 + return apply_filters('fluent_community/profile_courses_api_response', $data, $request->all());
259 669 }
260 670
261 671 public function getComments(Request $request, $userName)
262 672 {
@@ -263,9 +673,9 @@
263 673 $xProfile = XProfile::where('username', $userName)->first();
264 674
265 675 if (!$xProfile) {
266 676 return $this->sendError([
267 - 'message' => 'Profile not found'
677 + 'message' => __('Profile not found', 'fluent-community')
268 678 ]);
269 679 }
270 680
271 681 $currentUser = $this->getUser();
@@ -271,64 +681,114 @@
271 681 $currentUser = $this->getUser();
272 682 $hasAllAccess = $xProfile->user_id == get_current_user_id() || ($currentUser && $currentUser->isCommunityModerator());
273 683
274 684 $comments = Comment::where('user_id', $xProfile->user_id)
685 + ->where('status', 'published')
275 686 ->with([
276 - 'post' => function ($q) {
277 - $q->select(['id', 'title', 'message', 'type', 'space_id', 'slug', 'created_at'])
278 - ->with([
279 - 'space' => function ($q) {
280 - $q->select(['id', 'title', 'slug', 'type']);
281 - }
282 - ]);
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));
283 691 }
284 692 ])
285 - ->when(!$hasAllAccess, function ($q) use ($xProfile) {
286 - $q->whereHas('post', function ($query) use ($xProfile) {
693 + ->when(!$hasAllAccess, function ($q) {
694 + $q->whereHas('post', function ($query) {
287 695 $query->byUserAccess(get_current_user_id());
696 + $query->where('type', 'text');
288 697 });
289 698 })
290 699 ->orderBy('id', 'desc')
291 700 ->paginate();
292 701
293 - return [
294 - 'comments' => $comments,
295 - 'xprofile' => $xProfile
296 - ];
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());
297 718 }
298 719
299 720 public function getNotificationPreferance(Request $request, $userName)
300 721 {
301 - $xProfile = $this->verfifyAndGetProfile($userName);
722 + $emailPref = Utility::getEmailNotificationSettings();
302 723
724 + $xProfile = $this->verifyAndGetProfile($userName);
725 +
303 726 $globalPreferances = NotificationPref::getGlobalPrefs();
304 - $userPrefs = NotificationSubscription::where('user_id', $xProfile->user_id)
305 - ->select(['notification_type', 'is_read', 'object_id'])
306 - ->get();
307 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 +
308 740 $userGlobalPrefs = [];
309 741 $spaceWisePrefs = [];
310 - foreach ($userPrefs as $pref) {
311 - if (!$pref->object_id) {
312 - $userGlobalPrefs[$pref->notification_type] = $pref->is_read ? 'yes' : 'no';
313 - } else {
314 - if (empty($spaceWisePrefs[$pref->object_id])) {
315 - $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] = [];
316 753 }
317 - $spaceWisePrefs[$pref->object_id][$pref->notification_type] = $pref->is_read;
754 +
755 + $spaceWisePrefs[$spaceId][$matches[1]] = $prefValue;
756 + continue;
318 757 }
758 +
759 + $userGlobalPrefs[$prefKey] = $prefValue ? 'yes' : 'no';
319 760 }
320 761
321 - if (empty($userGlobalPrefs)) {
322 - $userGlobalPrefs = $globalPreferances;
323 - $userGlobalPrefs = array_map(function ($item) {
324 - return $item ? 'yes' : 'no';
325 - }, $userGlobalPrefs);
762 + $messagingConfig = Utility::getOption('_messaging_settings', []);
763 + $isGlobalPerUser = Arr::get($messagingConfig, 'messaging_email_frequency') == 'disabled';
764 +
765 + $pushAvailable = PushNotificationModule::isAvailable();
766 +
767 + $userGlobalPrefsDefaults = [
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'
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';
326 782 }
327 783
328 - $spaceGroups = SpaceGroup::with(['spaces' => function ($query) {
329 - $query->whereHas('members', function ($q) {
330 - $q->where('user_id', get_current_user_id());
784 + $userGlobalPrefs = wp_parse_args($userGlobalPrefs, $userGlobalPrefsDefaults);
785 +
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');
331 791 })
332 792 ->where('type', 'community');
333 793 }])
334 794 ->orderBy('serial', 'ASC')
@@ -334,14 +794,12 @@
334 794 ->orderBy('serial', 'ASC')
335 795 ->get();
336 796
337 797 $formattedSpaceGroups = [];
338 -
339 798 foreach ($spaceGroups as $group) {
340 799 if ($group->spaces->isEmpty()) {
341 800 continue;
342 801 }
343 -
344 802 $formattedSpaces = [];
345 803 foreach ($group->spaces as $space) {
346 804
347 805 $pref = '';
@@ -360,9 +818,8 @@
360 818 'icon' => $space->getIconMark(),
361 819 'pref' => $pref
362 820 ];
363 821 }
364 -
365 822 if ($formattedSpaces) {
366 823 $formattedSpaceGroups[] = [
367 824 'id' => $group->id,
368 825 'title' => $group->title,
@@ -370,27 +827,106 @@
370 827 ];
371 828 }
372 829 }
373 830
374 - return [
375 - 'user_globals' => (object)$userGlobalPrefs,
376 - 'spaceGroups' => $formattedSpaceGroups,
377 - 'space_prefs' => $spaceWisePrefs,
378 - 'digestEmailDay' => 'Monday'
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 +
867 + $digestDay = (string)Arr::get($emailPref, 'digest_mail_day', 'tue');
868 + if ($digestDay) {
869 + $maps = [
870 + 'mon' => __('Monday', 'fluent-community'),
871 + 'tue' => __('Tuesday', 'fluent-community'),
872 + 'wed' => __('Wednesday', 'fluent-community'),
873 + 'thu' => __('Thursday', 'fluent-community'),
874 + 'fri' => __('Friday', 'fluent-community'),
875 + 'sat' => __('Saturday', 'fluent-community'),
876 + 'sun' => __('Sunday', 'fluent-community'),
877 + ];
878 + if (isset($maps[$digestDay])) {
879 + $digestDay = $maps[$digestDay];
880 + }
881 + }
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 +
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,
379 899 ];
900 +
901 + return apply_filters('fluent_community/profile_notification_pref_api_response', $data, $request->all());
380 902 }
381 903
382 904 public function saveNotificationPreferance(Request $request, $userName)
383 905 {
384 - $xProfile = $this->verfifyAndGetProfile($userName);
906 + $xProfile = $this->verifyAndGetOwnProfile($userName);
385 907
386 908 $userPrefs = $request->get('user_globals', []);
387 909 $sapcePrefs = $request->get('space_prefs', []);
388 910
911 + $messagingPref = Arr::get($userPrefs, 'message_email_frequency');
912 +
389 913 $userPrefs = array_map(function ($item) {
390 914 return $item == 'yes' ? 1 : 0;
391 915 }, $userPrefs);
392 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 +
393 929 foreach ($sapcePrefs as $spaceId => $pref) {
394 930 $spaceId = (int)$spaceId;
395 931 if (!$pref || !$spaceId) {
396 932 continue;
@@ -403,24 +939,83 @@
403 939 $userPrefs['np_by_admin_mail_' . $spaceId] = 1;
404 940 }
405 941 }
406 942
407 -
408 943 NotificationPref::updateUserPrefs($xProfile->user_id, $userPrefs);
409 944
410 945 return [
411 946 'prefs' => $userPrefs,
412 - 'message' => __('Email Notification preferences has been updated', 'fluent-community')
947 + 'message' => __('Email Notification preferences have been updated', 'fluent-community')
413 948 ];
414 949 }
415 950
416 - private function verfifyAndGetProfile($userName)
951 + public function reconfirmEmail(Request $request, $userName)
417 952 {
953 + if (!defined('FLUENTCRM')) {
954 + return $this->sendError([
955 + 'message' => __('FluentCRM is not available on this site', 'fluent-community')
956 + ]);
957 + }
958 +
418 959 $xProfile = XProfile::where('username', $userName)->firstOrFail();
419 960
420 - $currentUser = $this->getUser();
421 - 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)) {
422 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'));
423 1018 }
424 1019
425 1020 return $xProfile;
426 1021 }