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

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