PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.9.0
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.9.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
fluent-community / app / Http / Controllers / ProfileController.php

ProfileController.php in FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses 2.9.0, at app/Http/Controllers/ProfileController.php

1,022 lines 40.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCommunity\App\Http\Controllers;
4
5 use FluentCommunity\App\Functions\Utility;
6 use FluentCommunity\App\Models\Comment;
7 use FluentCommunity\App\Models\Feed;
8 use FluentCommunity\App\Models\NotificationSubscription;
9 use FluentCommunity\App\Models\Space;
10 use FluentCommunity\App\Models\SpaceGroup;
11 use FluentCommunity\App\Models\SpaceUserPivot;
12 use FluentCommunity\App\Models\XProfile;
13 use FluentCommunity\App\Services\CustomSanitizer;
14 use FluentCommunity\App\Services\FeedsHelper;
15 use FluentCommunity\App\Services\Helper;
16 use FluentCommunity\App\Services\NotificationPref;
17 use FluentCommunity\App\Services\ProfileHelper;
18 use FluentCommunity\Framework\Http\Request\Request;
19 use FluentCommunity\Framework\Support\Arr;
20 use FluentCommunity\Modules\Course\Model\CourseLesson;
21 use FluentCommunity\Modules\Course\Model\CourseTopic;
22 use FluentCommunity\Modules\Course\Services\CourseHelper;
23 use FluentCommunity\Modules\PushNotification\PushNotificationModule;
24 use FluentCommunity\Framework\Foundation\Exceptions\HttpException;
25
26 class ProfileController extends Controller
27 {
28 public function getProfile(Request $request, $userName)
29 {
30 /** @var XProfile $xprofile */
31 $xprofile = XProfile::where('username', $userName)
32 ->firstOrFail();
33
34 if ($xprofile->status != 'active' && !Helper::isModerator()) {
35 return $this->sendError([
36 'message' => __('This profile is not active', 'fluent-community')
37 ], 403);
38 }
39
40 $canViewProfile = Utility::canViewUserProfile($xprofile->user_id);
41
42 $user = get_user_by('ID', $xprofile->user_id);
43
44 $profile = [
45 'user_id' => $xprofile->user_id,
46 'is_verified' => $xprofile->is_verified,
47 'display_name' => $xprofile->display_name,
48 'username' => $xprofile->username,
49 'avatar' => $xprofile->avatar,
50 'has_custom_avatar' => $xprofile->hasCustomAvatar(),
51 'cover_photo' => Arr::get($xprofile->meta, 'cover_photo'),
52 'headline' => Arr::get($xprofile->meta, 'headline', ''),
53 'total_points' => $xprofile->total_points,
54 'badge_slugs' => (array)Arr::get($xprofile->meta, 'badge_slug', []),
55 'status' => $xprofile->status,
56 'is_restricted' => !$canViewProfile,
57 'canViewUserSpaces' => ProfileHelper::canViewUserSpaces($xprofile->user_id, $this->getUser())
58 ];
59
60 if (Utility::showLastActivity()) {
61 $profile['last_activity'] = $xprofile->last_activity;
62 }
63
64 if ($canViewProfile) {
65 $profile['website'] = Arr::get($xprofile->meta, 'website');
66 $profile['created_at'] = $xprofile->created_at->format('Y-m-d H:i:s');
67 $profile['social_links'] = (object) Arr::get($xprofile->meta, 'social_links', []);
68 $profile['compilation_score'] = $xprofile->getCompletionScore();
69 $profile['short_description_rendered'] = wp_kses_post(FeedsHelper::mdToHtml($xprofile->short_description));
70 }
71
72 $currentUserId = get_current_user_id();
73
74 $isOwn = $xprofile->user_id == $currentUserId;
75
76 $isAdmin = Helper::isSiteAdmin($currentUserId);
77
78 if ($isOwn || $isAdmin) {
79 $enableUserSync = Utility::getPrivacySetting('enable_user_sync') === 'yes';
80 $nameArray = explode(' ', trim($xprofile->display_name));
81 $xprofileFirstName = array_shift($nameArray);
82 $xprofileLastName = implode(' ', $nameArray);
83
84 $profile['email'] = $user->user_email;
85 $profile['first_name'] = $enableUserSync ? $user->first_name : $xprofileFirstName;
86 $profile['last_name'] = $enableUserSync ? $user->last_name : $xprofileLastName;
87 $profile['short_description'] = $xprofile->short_description;
88 $profile['can_change_username'] = $isAdmin || Utility::getPrivacySetting('can_customize_username') === 'yes';
89 $profile['can_change_email'] = current_user_can('edit_users') || (Utility::getPrivacySetting('can_change_email') === 'yes' && $isOwn);
90 $profile['can_change_password'] = $isOwn && Utility::getPrivacySetting('can_change_password') === 'yes';
91 }
92
93 $profileBaseUrl = Helper::baseUrl('u/' . $xprofile->username . '/');
94
95 $profile['profile_navs'] = [
96 [
97 'slug' => 'user_profile',
98 'title' => __('About', 'fluent-community'),
99 'url' => $profileBaseUrl,
100 'wrapper_class' => 'fcom_profile_about',
101 'route' => [
102 'name' => 'user_profile'
103 ]
104 ],
105 [
106 'slug' => 'user_profile_feeds',
107 'title' => __('Posts', 'fluent-community'),
108 'wrapper_class' => 'fcom_profile_posts',
109 'url' => $profileBaseUrl . 'posts',
110 'route' => [
111 'name' => 'user_profile_feeds'
112 ]
113 ]
114 ];
115
116 if ($profile['canViewUserSpaces']) {
117 $profile['profile_navs'][] = [
118 'slug' => 'user_spaces',
119 'wrapper_class' => 'fcom_profile_spaces',
120 'title' => __('Spaces', 'fluent-community'),
121 'url' => $profileBaseUrl . 'spaces',
122 'route' => [
123 'name' => 'user_spaces'
124 ]
125 ];
126
127 if (Helper::isFeatureEnabled('course_module')) {
128 $profile['profile_navs'][] = [
129 'slug' => 'user_courses',
130 'wrapper_class' => 'fcom_profile_courses',
131 'title' => __('Courses', 'fluent-community'),
132 'url' => $profileBaseUrl . 'courses',
133 'route' => [
134 'name' => 'user_courses'
135 ]
136 ];
137 }
138 }
139
140 $profile['profile_navs'][] = [
141 'slug' => 'user_comments',
142 'wrapper_class' => 'fcom_profile_comments',
143 'title' => __('Comments', 'fluent-community'),
144 'url' => $profileBaseUrl . 'comments',
145 'route' => [
146 'name' => 'user_comments'
147 ]
148 ];
149
150 $profile['profile_nav_actions'] = [];
151
152 $profile = apply_filters('fluent_community/profile_view_data', $profile, $xprofile, $isAdmin);
153
154 return [
155 'profile' => $profile
156 ];
157 }
158
159 public function patchProfile(Request $request, $userName)
160 {
161 $xprofile = $this->verifyAndGetProfile($userName);
162
163 $updateData = $request->get('data', []);
164
165 if (!empty($updateData['status']) && $updateData['status'] === 'deactivated' && $xprofile->status === 'active') {
166 // handle deactivation
167 $canDeactivate = Utility::getPrivacySetting('can_deactive_account') === 'yes' || Helper::isSiteAdmin();
168 if (!$canDeactivate) {
169 return $this->sendError([
170 'message' => __('You are not allowed to deactivate this account.', 'fluent-community')
171 ]);
172 }
173
174 $xprofile->status = '';
175 $xprofile->save();
176 update_user_meta($xprofile->user_id, '_fcom_deactivated_at', current_time('mysql'));
177 do_action('fluent_community/profile_deactivated', $xprofile);
178
179 return [
180 'message' => __('Your profile has been deactivated successfully.', 'fluent-community')
181 ];
182 }
183
184 $mediaTypes = ['cover_photo', 'avatar'];
185
186 foreach ($mediaTypes as $type) {
187 if (!empty($updateData[$type])) {
188 $media = Helper::getMediaFromUrl($updateData[$type]);
189 if (!$media || $media->is_active) {
190 return $this->sendError([
191 'message' => __('Invalid media image. Please upload a new one.', 'fluent-community')
192 ]);
193 }
194
195 $updateData[$type] = $media->public_url;
196
197 $media->update([
198 'is_active' => true,
199 'user_id' => $xprofile->user_id,
200 'object_source' => 'user_' . $type
201 ]);
202 }
203 }
204
205 $deletedMedias = [];
206
207 if (isset($updateData['avatar'])) {
208
209 if ($xprofile->hasCustomAvatar()) {
210 $deletedMedias[] = $xprofile->attributes['avatar'];
211 }
212
213 $xprofile->avatar = $updateData['avatar'];
214
215 if (defined('FLUENTCRM')) {
216 $contact = $xprofile->contact;
217
218 if ($contact) {
219 $contact->update([
220 'avatar' => $updateData['avatar'] ?: null
221 ]);
222 }
223 }
224
225 if (empty($updateData['avatar'])) {
226 Utility::forgetCache('user_avatar_' . $xprofile->user_id);
227 }
228 }
229
230 if (isset($updateData['cover_photo'])) {
231 $deletedMedias[] = Arr::get($xprofile->meta, 'cover_photo');
232 $xprofile->meta = wp_parse_args(['cover_photo' => $updateData['cover_photo']], $xprofile->meta);
233 }
234
235 $xprofile->save();
236
237 if ($deletedMedias = array_filter($deletedMedias)) {
238 do_action('fluent_community/remove_medias_by_url', $deletedMedias, [
239 'user_id' => $xprofile->user_id,
240 'object_sources' => ['user_avatar', 'user_cover_photo']
241 ]);
242 }
243
244 return [
245 'message' => __('Profile updated', 'fluent-community')
246 ];
247 }
248
249 public function updateProfile(Request $request, $userName)
250 {
251 $currentUser = $this->getUser(true);
252 $data = $request->get('data', []);
253
254 /** @var XProfile $xProfile */
255 $xProfile = XProfile::where('username', $userName)->firstOrFail();
256
257 if ($xProfile->user_id != get_current_user_id()) {
258 if(!$currentUser->isCommunityModerator()) {
259 return $this->sendError([
260 'message' => __('You are not allowed to update this profile', 'fluent-community')
261 ]);
262 }
263 }
264
265 $this->validate($data, [
266 'first_name' => 'required',
267 ], [
268 'first_name.required' => __('First name is required', 'fluent-community')
269 ]);
270
271 $updateData = Arr::only($data, ['first_name', 'last_name', 'short_description', 'website']);
272
273 $updateData = apply_filters('fluent_community/update_profile_data', $updateData, $data, $xProfile, $currentUser);
274
275 $meta = $xProfile->meta;
276
277 $userNameChanged = false;
278
279 if ($currentUser->isCommunityModerator()) {
280 $updateData['is_verified'] = Arr::get($data, 'is_verified') ? 1 : 0;
281 $updateData['status'] = Arr::get($data, 'status', 'active');
282 $userName = Arr::get($data, 'username');
283
284 if (user_can($xProfile->user_id, 'list_users')) {
285 $updateData['status'] = 'active';
286 }
287
288 if ($userName) {
289 // Check if username is exit or not
290 $userName = CustomSanitizer::sanitizeUserName($userName);
291
292 if (!$userName) {
293 return $this->sendError([
294 'message' => __('Invalid username. Only Latin characters with _ & - are allowed.', 'fluent-community')
295 ]);
296 }
297
298 if (XProfile::where('username', $userName)->where('user_id', '!=', $xProfile->user_id)->exists()) {
299 return $this->sendError([
300 'message' => __('Community Username already taken by someone else', 'fluent-community')
301 ]);
302 }
303
304 $userExist = get_user_by('user_login', $userName);
305
306 if ($userExist && $userExist->ID != $xProfile->user_id) {
307 return $this->sendError([
308 'message' => __('Username already taken by someone else. Please use a different username.', 'fluent-community')
309 ]);
310 }
311
312 $updateData['username'] = $userName;
313 $userNameChanged = $userName != $xProfile->username;
314 }
315
316 if (Helper::isFeatureEnabled('user_badge')) {
317 $badgeSlug = array_filter((array) Arr::get($data, 'badge_slugs', []), 'is_scalar');
318 $badgeSlug = array_map('sanitize_text_field', $badgeSlug);
319
320 $definedBadges = (array) Utility::getOption('user_badges', []);
321 $meta['badge_slug'] = array_values(array_intersect($badgeSlug, array_keys($definedBadges)));
322 }
323 } else if (Utility::getPrivacySetting('can_customize_username')) {
324 $userName = Arr::get($data, 'username');
325
326 if ($xProfile->username != $userName) {
327 $userName = strtolower(CustomSanitizer::sanitizeUserName($userName));
328 if (!$userName) {
329 return $this->sendError([
330 'message' => __('Invalid username. Only Latin characters with _ & - are allowed.', 'fluent-community')
331 ]);
332 }
333
334 if (XProfile::where('username', $userName)->where('user_id', '!=', $xProfile->user_id)->exists()) {
335 return $this->sendError([
336 'message' => __('Community Username already taken by someone else', 'fluent-community')
337 ]);
338 }
339
340 if (strlen($userName) < 3) {
341 return $this->sendError([
342 'message' => __('Username should be at least 3 characters long.', 'fluent-community')
343 ]);
344 }
345
346 $reservedUserNames = ProfileHelper::getReservedUserNames();
347 if (in_array($userName, $reservedUserNames)) {
348 return $this->sendError([
349 'message' => __('Please use another username. This username is reserved', 'fluent-community')
350 ]);
351 }
352
353 $updateData['username'] = $userName;
354 $userNameChanged = true;
355 }
356 }
357
358 $updateData['display_name'] = trim(sanitize_text_field(Arr::get($data, 'first_name') . ' ' . Arr::get($data, 'last_name')));
359
360 $updateData['short_description'] = CustomSanitizer::unslashMarkdown(sanitize_textarea_field(trim(Arr::get($data, 'short_description'))));
361 $meta['website'] = sanitize_url(Arr::get($data, 'website'));
362 $meta['headline'] = sanitize_text_field(trim(Arr::get($data, 'headline', '')));
363 $socialLinks = Arr::get($data, 'social_links', []);
364
365 $maxDescriptionLength = apply_filters('fluent_community/max_profile_description_length', 5000);
366 if ($updateData['short_description'] && strlen($updateData['short_description']) > $maxDescriptionLength) {
367 return $this->sendError([
368 'message' => sprintf(
369 /* translators: %d: Maximum number of characters allowed in the profile bio. */
370 __('Profile bio should not exceed %d characters.', 'fluent-community'),
371 $maxDescriptionLength
372 )
373 ]);
374 }
375
376 $maxHeadlineLength = apply_filters('fluent_community/max_profile_headline_length', 60);
377 if ($meta['headline'] && mb_strlen($meta['headline']) > $maxHeadlineLength) {
378 return $this->sendError([
379 'message' => sprintf(
380 /* translators: %d: Maximum number of characters allowed in the profile headline. */
381 __('Headline should not exceed %d characters.', 'fluent-community'),
382 $maxHeadlineLength
383 )
384 ]);
385 }
386
387 if ($socialLinks) {
388 $socialLinks = array_filter($socialLinks);
389 $formattedSocialLinkes = [];
390 $socialLinkProviders = ProfileHelper::socialLinkProviders(true);
391 foreach ($socialLinks as $linkName => $socialLink) {
392 if (isset($socialLinkProviders[$linkName])) {
393 $formattedSocialLinkes[$linkName] = sanitize_text_field(trim($socialLink));
394 }
395 }
396 $meta['social_links'] = $formattedSocialLinkes;
397 }
398
399 $meta['short_description_rendered'] = wp_kses_post(FeedsHelper::mdToHtml($updateData['short_description']));
400
401 $updateData['meta'] = $meta;
402
403 $xProfile->fill($updateData);
404 $xProfile->save();
405
406 // Let's update the user's details
407 $xProfile->user->updateCustomData($updateData);
408 $xProfile->compilation_score = $xProfile->getCompletionScore();
409
410 if ($userNameChanged) {
411 return [
412 'message' => __('Profile has been updated', 'fluent-community'),
413 'profile' => $xProfile,
414 'redirect_url' => Helper::baseUrl('u/' . $xProfile->username . '/update')
415 ];
416 }
417
418 $isOwn = $xProfile->user_id == get_current_user_id();
419 $canEditUsers = current_user_can('edit_users');
420 if ($canEditUsers || (Utility::getPrivacySetting('can_change_email') === 'yes' && $isOwn)) {
421 $emailAddress = Arr::get($data, 'email');
422
423 if ($emailAddress && is_email($emailAddress) && $emailAddress != $xProfile->user->user_email) {
424 $owner_id = email_exists($emailAddress);
425 if ($owner_id && $owner_id != $xProfile->user_id) {
426 return $this->sendError([
427 'message' => __('Email address already taken by someone else. Please use a different email address.', 'fluent-community')
428 ]);
429 }
430
431 // Let's check if it's their own
432 $requireVerification = $isOwn && !$canEditUsers;
433 if ($requireVerification) {
434 $currentUser = get_user_by('ID', $xProfile->user_id);
435 ProfileHelper::sendConfirmationOnProfileEmailChange($currentUser, $emailAddress);
436 return [
437 'message' => __('Email address change is pending. Please check your inbox to verify the new email address.', 'fluent-community'),
438 'profile' => $xProfile
439 ];
440 }
441
442 wp_update_user([
443 'user_email' => $emailAddress,
444 'ID' => $xProfile->user_id
445 ]);
446 }
447 }
448
449 return [
450 'message' => __('Profile has been updated', 'fluent-community'),
451 'profile' => $xProfile
452 ];
453 }
454
455 public function changePassword(Request $request, $userName)
456 {
457 $xProfile = XProfile::where('username', $userName)->firstOrFail();
458
459 // Password can only be changed by the account owner, never by moderators/admins here.
460 if ($xProfile->user_id != get_current_user_id()) {
461 return $this->sendError([
462 'message' => __('You are not allowed to change this password', 'fluent-community')
463 ]);
464 }
465
466 if (Utility::getPrivacySetting('can_change_password') !== 'yes') {
467 return $this->sendError([
468 'message' => __('Password change is disabled', 'fluent-community')
469 ]);
470 }
471
472 $data = $request->get('data', []);
473
474 $this->validate($data, [
475 'current_password' => 'required',
476 'new_password' => 'required',
477 'confirm_password' => 'required',
478 ], [
479 'current_password.required' => __('Current password is required', 'fluent-community'),
480 'new_password.required' => __('New password is required', 'fluent-community'),
481 'confirm_password.required' => __('Please confirm your new password', 'fluent-community'),
482 ]);
483
484 // Passwords are used verbatim; sanitizing would corrupt valid characters.
485 $currentPassword = (string) Arr::get($data, 'current_password');
486 $newPassword = (string) Arr::get($data, 'new_password');
487 $confirmPassword = (string) Arr::get($data, 'confirm_password');
488
489 if (strlen($newPassword) < 4) {
490 return $this->sendError([
491 'message' => __('New password must be at least 4 characters long', 'fluent-community')
492 ]);
493 }
494
495 if ($newPassword !== $confirmPassword) {
496 return $this->sendError([
497 'message' => __('New password and confirmation do not match', 'fluent-community')
498 ]);
499 }
500
501 if ($newPassword === $currentPassword) {
502 return $this->sendError([
503 'message' => __('New password must be different from your current password', 'fluent-community')
504 ]);
505 }
506
507 $user = get_user_by('id', $xProfile->user_id);
508
509 if (!$user || !wp_check_password($currentPassword, $user->user_pass, $user->ID)) {
510 return $this->sendError([
511 'message' => __('Your current password is incorrect', 'fluent-community')
512 ]);
513 }
514
515 wp_set_password($newPassword, $user->ID);
516
517 // wp_set_password destroys every session for the user, which also invalidates the
518 // REST nonce the SPA holds. Re-issue the cookie to keep the session, capturing the
519 // fresh logged-in cookie so the nonces we mint below bind to the new session token.
520 $newLoggedInCookie = '';
521 $captureLoggedInCookie = function ($loggedInCookie) use (&$newLoggedInCookie) {
522 $newLoggedInCookie = $loggedInCookie;
523 };
524 add_action('set_logged_in_cookie', $captureLoggedInCookie);
525
526 wp_set_current_user($user->ID);
527 wp_set_auth_cookie($user->ID, true);
528
529 remove_action('set_logged_in_cookie', $captureLoggedInCookie);
530
531 if ($newLoggedInCookie) {
532 $_COOKIE[LOGGED_IN_COOKIE] = $newLoggedInCookie;
533 }
534
535 do_action('fluent_community/user/password_changed', $user->ID);
536
537 return [
538 'message' => __('Your password has been changed successfully', 'fluent-community'),
539 'rest_nonce' => wp_create_nonce('wp_rest'),
540 'ajax_nonce' => wp_create_nonce('fluent_community_ajax_nonce'),
541 ];
542 }
543
544 public function getAllMemberships(Request $request, $userName)
545 {
546 /** @var XProfile $xProfile */
547 $xProfile = XProfile::where('username', $userName)->firstOrFail();
548
549 $currentUser = $this->getUser();
550
551 if (!ProfileHelper::canViewUserSpaces($xProfile->user_id, $currentUser)) {
552 return $this->sendError([
553 'message' => __('You are not allowed to view this profile\'s membership.', 'fluent-community'),
554 'permission_failed' => true
555 ]);
556 }
557
558 $canSeeSecret = $xProfile->user_id == get_current_user_id()
559 || ($currentUser && $currentUser->isCommunityModerator());
560
561 $memberships = $xProfile->spaces()
562 ->wherePivot('status', 'active')
563 ->when(!$canSeeSecret, function ($q) {
564 $q->whereIn('privacy', ['public', 'private']);
565 })
566 ->get()
567 ->pluck('id');
568
569 return apply_filters('fluent_community/profile_all_memberships_api_response', [
570 'memberships' => $memberships
571 ], $request->all());
572 }
573
574 public function getSpaces(Request $request, $userName)
575 {
576 /** @var XProfile $xProfile */
577 $xProfile = XProfile::where('username', $userName)->firstOrFail();
578 $currentUser = $this->getUser();
579
580 if (!ProfileHelper::canViewUserSpaces($xProfile->user_id, $currentUser)) {
581 return $this->sendError([
582 'message' => __('You are not allowed to view this profile\'s spaces.', 'fluent-community'),
583 'permission_failed' => true
584 ]);
585 }
586
587 if ($xProfile->user_id == get_current_user_id() || ($currentUser && $currentUser->isCommunityModerator())) {
588 $spaces = $xProfile->spaces()
589 ->wherePivot('status', 'active')
590 ->get();
591 } else {
592 $spaces = $xProfile->spaces()
593 ->whereIn('privacy', ['public', 'private'])
594 ->wherePivot('status', 'active')
595 ->get();
596 }
597
598 foreach ($spaces as $space) {
599 $shouldHideMembersCount = Arr::get($space->settings, 'hide_members_count') == 'yes';
600 $canViewMembers = $currentUser && $space->verifyUserPermisson($currentUser, 'can_view_members', false);
601 if ($shouldHideMembersCount && !$canViewMembers) {
602 $space->members_count = 0;
603 continue;
604 }
605 $space->members_count = $space->members()->count();
606 }
607
608 $data = [
609 'spaces' => $spaces
610 ];
611
612 return apply_filters('fluent_community/profile_spaces_api_response', $data, $request->all());
613 }
614
615 public function getCourses(Request $request, $userName)
616 {
617 if (!Helper::isFeatureEnabled('course_module')) {
618 return $this->sendError([
619 'message' => __('Course module is disabled.', 'fluent-community')
620 ]);
621 }
622
623 /** @var XProfile $xProfile */
624 $xProfile = XProfile::where('username', $userName)->firstOrFail();
625 $currentUser = $this->getUser();
626
627 if (!ProfileHelper::canViewUserSpaces($xProfile->user_id, $currentUser)) {
628 return $this->sendError([
629 'message' => __('You are not allowed to view this profile\'s courses.', 'fluent-community'),
630 'permission_failed' => true
631 ]);
632 }
633
634 $hasAllAccess = $xProfile->user_id == get_current_user_id() || ($currentUser && $currentUser->isCommunityModerator());
635
636 $courses = $xProfile->courses()
637 ->wherePivot('status', 'active')
638 ->where('fcom_spaces.status', 'published')
639 ->when(!$hasAllAccess, function ($q) {
640 $q->whereIn('fcom_spaces.privacy', ['public', 'private']);
641 })
642 ->get();
643
644 foreach ($courses as $course) {
645 $course->isEnrolled = CourseHelper::isEnrolled($course->id, $xProfile->user_id);
646 if ($course->isEnrolled) {
647 $course->progress = CourseHelper::getCourseProgress($course->id, $xProfile->user_id);
648 }
649
650 if (!$course->cover_photo) {
651 $course->cover_photo = FLUENT_COMMUNITY_PLUGIN_URL . 'assets/images/course-placeholder.jpg';
652 }
653
654 $course->sectionsCount = CourseTopic::where('space_id', $course->id)->count();
655 $course->lessonsCount = CourseLesson::where('space_id', $course->id)->count();
656 if (Arr::get($course->settings, 'hide_members_count') != 'yes') {
657 $course->studentsCount = SpaceUserPivot::where('space_id', $course->id)->count();
658 } else {
659 $course->studentsCount = 0;
660 }
661
662 do_action_ref_array('fluent_community/course', [&$course]);
663 }
664
665 $data = [
666 'courses' => $courses
667 ];
668
669 return apply_filters('fluent_community/profile_courses_api_response', $data, $request->all());
670 }
671
672 public function getComments(Request $request, $userName)
673 {
674 $xProfile = XProfile::where('username', $userName)->first();
675
676 if (!$xProfile) {
677 return $this->sendError([
678 'message' => __('Profile not found', 'fluent-community')
679 ]);
680 }
681
682 $currentUser = $this->getUser();
683 $hasAllAccess = $xProfile->user_id == get_current_user_id() || ($currentUser && $currentUser->isCommunityModerator());
684
685 $comments = Comment::where('user_id', $xProfile->user_id)
686 ->where('status', 'published')
687 ->with([
688 'post' => function ($q) use ($currentUser) {
689 // Eager load the full feed so the post opens in the modal without a per-click fetch.
690 $q->select(array_merge(Feed::$publicColumns, ['message']))
691 ->with(Feed::withPublicRelations($currentUser));
692 }
693 ])
694 ->when(!$hasAllAccess, function ($q) {
695 $q->whereHas('post', function ($query) {
696 $query->byUserAccess(get_current_user_id());
697 $query->where('type', 'text');
698 });
699 })
700 ->orderBy('id', 'desc')
701 ->paginate();
702
703 $posts = $comments->getCollection()
704 ->pluck('post')
705 ->filter()
706 ->unique('id')
707 ->values();
708
709 if ($posts->isNotEmpty()) {
710 FeedsHelper::transformFeedsCollection($posts);
711 }
712
713 $data = [
714 'comments' => $comments,
715 'xprofile' => $xProfile
716 ];
717
718 return apply_filters('fluent_community/profile_comments_api_response', $data, $request->all());
719 }
720
721 public function getNotificationPreferance(Request $request, $userName)
722 {
723 $emailPref = Utility::getEmailNotificationSettings();
724
725 $xProfile = $this->verifyAndGetProfile($userName);
726
727 $globalPreferances = NotificationPref::getGlobalPrefs();
728
729 $userPrefs = NotificationSubscription::where('user_id', $xProfile->user_id)
730 ->select(['notification_type', 'is_read', 'object_id'])
731 ->get();
732
733 $userGlobalPrefs = [];
734 $spaceWisePrefs = [];
735 foreach ($userPrefs as $pref) {
736 if (!$pref->object_id) {
737 if ($pref->notification_type === 'message_email_frequency') {
738 $maps = [
739 0 => 'disabled',
740 1 => 'hourly',
741 2 => 'daily',
742 3 => 'weekly'
743 ];
744
745 if (isset($maps[$pref->is_read])) {
746 $userGlobalPrefs[$pref->notification_type] = $maps[$pref->is_read];
747 } else {
748 $userGlobalPrefs[$pref->notification_type] = 'default';
749 }
750 continue;
751 }
752 $userGlobalPrefs[$pref->notification_type] = $pref->is_read ? 'yes' : 'no';
753 } else {
754 if (empty($spaceWisePrefs[$pref->object_id])) {
755 $spaceWisePrefs[$pref->object_id] = [];
756 }
757 $spaceWisePrefs[$pref->object_id][$pref->notification_type] = $pref->is_read;
758 }
759 }
760
761 $messagingConfig = Utility::getOption('_messaging_settings', []);
762 $isGlobalPerUser = Arr::get($messagingConfig, 'messaging_email_frequency') == 'disabled';
763
764 $pushAvailable = PushNotificationModule::isAvailable();
765
766 $userGlobalPrefsDefaults = [
767 'digest_mail' => Arr::get($globalPreferances, 'digest_email_status') ? 'yes' : 'no',
768 'mention_mail' => Arr::get($globalPreferances, 'mention_mail') ? 'yes' : 'no',
769 'reply_my_com_mail' => Arr::get($globalPreferances, 'reply_my_com_mail') ? 'yes' : 'no',
770 'com_my_post_mail' => Arr::get($globalPreferances, 'com_my_post_mail') ? 'yes' : 'no',
771 'message_email_frequency' => $isGlobalPerUser ? 'disabled' : 'default'
772 ];
773
774 if ($pushAvailable) {
775 $pushPreferances = NotificationPref::getGlobalPrefs('push');
776
777 $userGlobalPrefsDefaults['com_my_post_push'] = Arr::get($pushPreferances, 'com_my_post_push') ? 'yes' : 'no';
778 $userGlobalPrefsDefaults['reply_my_com_push'] = Arr::get($pushPreferances, 'reply_my_com_push') ? 'yes' : 'no';
779 $userGlobalPrefsDefaults['mention_push'] = Arr::get($pushPreferances, 'mention_push') ? 'yes' : 'no';
780 $userGlobalPrefsDefaults['co_com_push'] = Arr::get($pushPreferances, 'co_com_push') ? 'yes' : 'no';
781 }
782
783 $userGlobalPrefs = wp_parse_args($userGlobalPrefs, $userGlobalPrefsDefaults);
784
785 $profileUserId = $xProfile->user_id;
786 $spaceGroups = SpaceGroup::with(['spaces' => function ($query) use ($profileUserId) {
787 $query->whereHas('members', function ($q) use ($profileUserId) {
788 $q->where('user_id', $profileUserId)
789 ->where('status', 'active');
790 })
791 ->where('type', 'community');
792 }])
793 ->orderBy('serial', 'ASC')
794 ->get();
795
796 $formattedSpaceGroups = [];
797 foreach ($spaceGroups as $group) {
798 if ($group->spaces->isEmpty()) {
799 continue;
800 }
801 $formattedSpaces = [];
802 foreach ($group->spaces as $space) {
803
804 $pref = '';
805 if (isset($spaceWisePrefs[$space->id])) {
806 $perfs = (array)$spaceWisePrefs[$space->id];
807 if (!empty($perfs['np_by_member_mail'])) {
808 $pref = 'all_member_posts';
809 } else if (!empty($perfs['np_by_admin_mail'])) {
810 $pref = 'admin_only_posts';
811 }
812 }
813
814 $formattedSpaces[] = [
815 'id' => $space->id,
816 'title' => $space->title,
817 'icon' => $space->getIconMark(),
818 'pref' => $pref
819 ];
820 }
821 if ($formattedSpaces) {
822 $formattedSpaceGroups[] = [
823 'id' => $group->id,
824 'title' => $group->title,
825 'spaces' => $formattedSpaces
826 ];
827 }
828 }
829
830 // let's find the other spaces
831 $otherSpaces = Space::whereHas('members', function ($q) use ($xProfile) {
832 $q->where('user_id', $xProfile->user_id);
833 })
834 ->whereNull('parent_id')
835 ->orderBy('title', 'ASC')
836 ->get();
837
838 if (!$otherSpaces->isEmpty()) {
839 $formattedSpaces = [];
840 foreach ($otherSpaces as $space) {
841 $pref = '';
842 if (isset($spaceWisePrefs[$space->id])) {
843 $perfs = (array)$spaceWisePrefs[$space->id];
844 if (!empty($perfs['np_by_member_mail'])) {
845 $pref = 'all_member_posts';
846 } else if (!empty($perfs['np_by_admin_mail'])) {
847 $pref = 'admin_only_posts';
848 }
849 }
850
851 $formattedSpaces[] = [
852 'id' => $space->id,
853 'title' => $space->title,
854 'icon' => $space->getIconMark(),
855 'pref' => $pref
856 ];
857 }
858
859 $formattedSpaceGroups[] = [
860 'id' => 'other_space_group',
861 'title' => __('Other Spaces', 'fluent-community'),
862 'spaces' => $formattedSpaces
863 ];
864 }
865
866 $digestDay = (string)Arr::get($emailPref, 'digest_mail_day', 'tue');
867 if ($digestDay) {
868 $maps = [
869 'mon' => __('Monday', 'fluent-community'),
870 'tue' => __('Tuesday', 'fluent-community'),
871 'wed' => __('Wednesday', 'fluent-community'),
872 'thu' => __('Thursday', 'fluent-community'),
873 'fri' => __('Friday', 'fluent-community'),
874 'sat' => __('Saturday', 'fluent-community'),
875 'sun' => __('Sunday', 'fluent-community'),
876 ];
877 if (isset($maps[$digestDay])) {
878 $digestDay = $maps[$digestDay];
879 }
880 }
881
882 $crmEmailStatus = '';
883 if ($xProfile->user_id == get_current_user_id()) {
884 $profileUser = get_user_by('ID', $xProfile->user_id);
885 if ($profileUser && $profileUser->user_email) {
886 $crmEmailStatus = Helper::getCrmUndeliverableStatus($profileUser->user_email);
887 }
888 }
889
890 $data = [
891 'user_globals' => (object)$userGlobalPrefs,
892 'spaceGroups' => $formattedSpaceGroups,
893 'space_prefs' => $spaceWisePrefs,
894 'digestEmailDay' => $digestDay,
895 'default_messaging_email_frequency' => Arr::get($messagingConfig, 'messaging_email_status') !== 'yes' ? 'no' : Arr::get($messagingConfig, 'messaging_email_frequency'),
896 'crm_email_status' => $crmEmailStatus,
897 'push_available' => $pushAvailable,
898 ];
899
900 return apply_filters('fluent_community/profile_notification_pref_api_response', $data, $request->all());
901 }
902
903 public function saveNotificationPreferance(Request $request, $userName)
904 {
905 $xProfile = $this->verifyAndGetOwnProfile($userName);
906
907 $userPrefs = $request->get('user_globals', []);
908 $sapcePrefs = $request->get('space_prefs', []);
909
910 $messagingPref = Arr::get($userPrefs, 'message_email_frequency');
911
912 $userPrefs = array_map(function ($item) {
913 return $item == 'yes' ? 1 : 0;
914 }, $userPrefs);
915
916 if ($messagingPref == 'hourly') {
917 $userPrefs['message_email_frequency'] = 1;
918 } else if ($messagingPref == 'daily') {
919 $userPrefs['message_email_frequency'] = 2;
920 } else if ($messagingPref == 'disabled') {
921 $userPrefs['message_email_frequency'] = 0;
922 } else if ($messagingPref == 'weekly') {
923 $userPrefs['message_email_frequency'] = 3;
924 } else {
925 unset($userPrefs['message_email_frequency']);
926 }
927
928 foreach ($sapcePrefs as $spaceId => $pref) {
929 $spaceId = (int)$spaceId;
930 if (!$pref || !$spaceId) {
931 continue;
932 }
933
934 if ($pref == 'all_member_posts') {
935 $userPrefs['np_by_member_mail_' . $spaceId] = 1;
936 $userPrefs['np_by_admin_mail_' . $spaceId] = 1;
937 } else if ($pref == 'admin_only_posts') {
938 $userPrefs['np_by_admin_mail_' . $spaceId] = 1;
939 }
940 }
941
942 NotificationPref::updateUserPrefs($xProfile->user_id, $userPrefs);
943
944 return [
945 'prefs' => $userPrefs,
946 'message' => __('Email Notification preferences have been updated', 'fluent-community')
947 ];
948 }
949
950 public function reconfirmEmail(Request $request, $userName)
951 {
952 if (!defined('FLUENTCRM')) {
953 return $this->sendError([
954 'message' => __('FluentCRM is not available on this site', 'fluent-community')
955 ]);
956 }
957
958 $xProfile = XProfile::where('username', $userName)->firstOrFail();
959
960 if ($xProfile->user_id != get_current_user_id()) {
961 return $this->sendError([
962 'message' => __('You can only re-confirm your own email address', 'fluent-community')
963 ]);
964 }
965
966 $profileUser = get_user_by('ID', $xProfile->user_id);
967 $email = $profileUser ? $profileUser->user_email : '';
968
969 if (!$email || !Helper::getCrmUndeliverableStatus($email)) {
970 return $this->sendError([
971 'message' => __('Your email address does not need re-confirmation', 'fluent-community')
972 ]);
973 }
974
975 $subscriber = \FluentCrm\App\Models\Subscriber::where('email', $email)->first();
976
977 if (!$subscriber) {
978 return $this->sendError([
979 'message' => __('Your email address does not need re-confirmation', 'fluent-community')
980 ]);
981 }
982
983 // In-memory only, never saved: the opt-in sender is gated on status == 'pending'
984 // and does not persist the subscriber, so the stored status stays untouched
985 // and FluentCommunity keeps pausing emails until the confirmation link is clicked.
986 $subscriber->status = 'pending';
987
988 if (!$subscriber->sendDoubleOptinEmail()) {
989 return $this->sendError([
990 'message' => __('The confirmation email could not be sent right now. Please try again after a few minutes.', 'fluent-community')
991 ]);
992 }
993
994 return [
995 'message' => __('A confirmation email has been sent. Please check your inbox and click the confirmation link to resume email notifications.', 'fluent-community')
996 ];
997 }
998
999 private function verifyAndGetProfile($userName)
1000 {
1001 $xProfile = XProfile::where('username', $userName)->firstOrFail();
1002
1003 $currentUserId = get_current_user_id();
1004 if ($xProfile->user_id != $currentUserId && !Helper::isSuperAdmin($currentUserId)) {
1005 throw new \Exception('You are not allowed to update this profile');
1006 }
1007
1008 return $xProfile;
1009 }
1010
1011 private function verifyAndGetOwnProfile($userName)
1012 {
1013 $xProfile = XProfile::where('username', $userName)->firstOrFail();
1014
1015 if (!get_current_user_id() || $xProfile->user_id != get_current_user_id()) {
1016 throw new HttpException(403, esc_html__('You are not allowed to access these notification preferences.', 'fluent-community'));
1017 }
1018
1019 return $xProfile;
1020 }
1021 }
1022