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

1,023 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\Space;
9 use FluentCommunity\App\Models\SpaceGroup;
10 use FluentCommunity\App\Models\SpaceUserPivot;
11 use FluentCommunity\App\Models\XProfile;
12 use FluentCommunity\App\Services\CustomSanitizer;
13 use FluentCommunity\App\Services\FeedsHelper;
14 use FluentCommunity\App\Services\Helper;
15 use FluentCommunity\App\Services\NotificationPref;
16 use FluentCommunity\App\Services\ProfileHelper;
17 use FluentCommunity\Framework\Http\Request\Request;
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;
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((string) $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[] = Arr::get($xprofile->getAttributes(), '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_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)));
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 }
355 }
356
357 $updateData['display_name'] = trim(sanitize_text_field(Arr::get($data, 'first_name') . ' ' . Arr::get($data, 'last_name')));
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', '')));
362 $socialLinks = Arr::get($data, 'social_links', []);
363
364 $maxDescriptionLength = apply_filters('fluent_community/max_profile_description_length', 5000);
365 if ($updateData['short_description'] && strlen($updateData['short_description']) > $maxDescriptionLength) {
366 return $this->sendError([
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 )
372 ]);
373 }
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
386 if ($socialLinks) {
387 $socialLinks = array_filter($socialLinks);
388 $formattedSocialLinkes = [];
389 $socialLinkProviders = ProfileHelper::socialLinkProviders(true);
390 foreach ($socialLinks as $linkName => $socialLink) {
391 if (isset($socialLinkProviders[$linkName])) {
392 $formattedSocialLinkes[$linkName] = sanitize_text_field(trim($socialLink));
393 }
394 }
395 $meta['social_links'] = $formattedSocialLinkes;
396 }
397
398 $meta['short_description_rendered'] = wp_kses_post(FeedsHelper::mdToHtml($updateData['short_description']));
399
400 $updateData['meta'] = $meta;
401
402 $xProfile->fill($updateData);
403 $xProfile->save();
404
405 // Let's update the user's details
406 $xProfile->user->updateCustomData($updateData);
407 $xProfile->compilation_score = $xProfile->getCompletionScore();
408
409 if ($userNameChanged) {
410 return [
411 'message' => __('Profile has been updated', 'fluent-community'),
412 'profile' => $xProfile,
413 'redirect_url' => Helper::baseUrl('u/' . $xProfile->username . '/update')
414 ];
415 }
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
448 return [
449 'message' => __('Profile has been updated', 'fluent-community'),
450 'profile' => $xProfile
451 ];
452 }
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
573 public function getSpaces(Request $request, $userName)
574 {
575 /** @var XProfile $xProfile */
576 $xProfile = XProfile::where('username', $userName)->firstOrFail();
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
586 if ($xProfile->user_id == get_current_user_id() || ($currentUser && $currentUser->isCommunityModerator())) {
587 $spaces = $xProfile->spaces()
588 ->wherePivot('status', 'active')
589 ->get();
590 } else {
591 $spaces = $xProfile->spaces()
592 ->whereIn('privacy', ['public', 'private'])
593 ->wherePivot('status', 'active')
594 ->get();
595 }
596
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 }
604 $space->members_count = $space->members()->count();
605 }
606
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
666 ];
667
668 return apply_filters('fluent_community/profile_courses_api_response', $data, $request->all());
669 }
670
671 public function getComments(Request $request, $userName)
672 {
673 $xProfile = XProfile::where('username', $userName)->first();
674
675 if (!$xProfile) {
676 return $this->sendError([
677 'message' => __('Profile not found', 'fluent-community')
678 ]);
679 }
680
681 $currentUser = $this->getUser();
682 $hasAllAccess = $xProfile->user_id == get_current_user_id() || ($currentUser && $currentUser->isCommunityModerator());
683
684 $comments = Comment::where('user_id', $xProfile->user_id)
685 ->where('status', 'published')
686 ->with([
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));
691 }
692 ])
693 ->when(!$hasAllAccess, function ($q) {
694 $q->whereHas('post', function ($query) {
695 $query->byUserAccess(get_current_user_id());
696 $query->where('type', 'text');
697 });
698 })
699 ->orderBy('id', 'desc')
700 ->paginate();
701
702 $posts = $comments->getCollection()
703 ->pluck('post')
704 ->filter()
705 ->unique('id')
706 ->values();
707
708 if ($posts->isNotEmpty()) {
709 FeedsHelper::transformFeedsCollection($posts);
710 }
711
712 $data = [
713 'comments' => $comments,
714 'xprofile' => $xProfile
715 ];
716
717 return apply_filters('fluent_community/profile_comments_api_response', $data, $request->all());
718 }
719
720 public function getNotificationPreferance(Request $request, $userName)
721 {
722 $emailPref = Utility::getEmailNotificationSettings();
723
724 $xProfile = $this->verifyAndGetProfile($userName);
725
726 $globalPreferances = NotificationPref::getGlobalPrefs();
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
740 $userGlobalPrefs = [];
741 $spaceWisePrefs = [];
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] = [];
753 }
754
755 $spaceWisePrefs[$spaceId][$matches[1]] = $prefValue;
756 continue;
757 }
758
759 $userGlobalPrefs[$prefKey] = $prefValue ? 'yes' : 'no';
760 }
761
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';
782 }
783
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');
791 })
792 ->where('type', 'community');
793 }])
794 ->orderBy('serial', 'ASC')
795 ->get();
796
797 $formattedSpaceGroups = [];
798 foreach ($spaceGroups as $group) {
799 if ($group->spaces->isEmpty()) {
800 continue;
801 }
802 $formattedSpaces = [];
803 foreach ($group->spaces as $space) {
804
805 $pref = '';
806 if (isset($spaceWisePrefs[$space->id])) {
807 $perfs = (array)$spaceWisePrefs[$space->id];
808 if (!empty($perfs['np_by_member_mail'])) {
809 $pref = 'all_member_posts';
810 } else if (!empty($perfs['np_by_admin_mail'])) {
811 $pref = 'admin_only_posts';
812 }
813 }
814
815 $formattedSpaces[] = [
816 'id' => $space->id,
817 'title' => $space->title,
818 'icon' => $space->getIconMark(),
819 'pref' => $pref
820 ];
821 }
822 if ($formattedSpaces) {
823 $formattedSpaceGroups[] = [
824 'id' => $group->id,
825 'title' => $group->title,
826 'spaces' => $formattedSpaces
827 ];
828 }
829 }
830
831 // let's find the other spaces
832 $otherSpaces = Space::whereHas('members', function ($q) use ($xProfile) {
833 $q->where('user_id', $xProfile->user_id);
834 })
835 ->whereNull('parent_id')
836 ->orderBy('title', 'ASC')
837 ->get();
838
839 if (!$otherSpaces->isEmpty()) {
840 $formattedSpaces = [];
841 foreach ($otherSpaces as $space) {
842 $pref = '';
843 if (isset($spaceWisePrefs[$space->id])) {
844 $perfs = (array)$spaceWisePrefs[$space->id];
845 if (!empty($perfs['np_by_member_mail'])) {
846 $pref = 'all_member_posts';
847 } else if (!empty($perfs['np_by_admin_mail'])) {
848 $pref = 'admin_only_posts';
849 }
850 }
851
852 $formattedSpaces[] = [
853 'id' => $space->id,
854 'title' => $space->title,
855 'icon' => $space->getIconMark(),
856 'pref' => $pref
857 ];
858 }
859
860 $formattedSpaceGroups[] = [
861 'id' => 'other_space_group',
862 'title' => __('Other Spaces', 'fluent-community'),
863 'spaces' => $formattedSpaces
864 ];
865 }
866
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,
899 ];
900
901 return apply_filters('fluent_community/profile_notification_pref_api_response', $data, $request->all());
902 }
903
904 public function saveNotificationPreferance(Request $request, $userName)
905 {
906 $xProfile = $this->verifyAndGetOwnProfile($userName);
907
908 $userPrefs = $request->get('user_globals', []);
909 $sapcePrefs = $request->get('space_prefs', []);
910
911 $messagingPref = Arr::get($userPrefs, 'message_email_frequency');
912
913 $userPrefs = array_map(function ($item) {
914 return $item == 'yes' ? 1 : 0;
915 }, $userPrefs);
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
929 foreach ($sapcePrefs as $spaceId => $pref) {
930 $spaceId = (int)$spaceId;
931 if (!$pref || !$spaceId) {
932 continue;
933 }
934
935 if ($pref == 'all_member_posts') {
936 $userPrefs['np_by_member_mail_' . $spaceId] = 1;
937 $userPrefs['np_by_admin_mail_' . $spaceId] = 1;
938 } else if ($pref == 'admin_only_posts') {
939 $userPrefs['np_by_admin_mail_' . $spaceId] = 1;
940 }
941 }
942
943 NotificationPref::updateUserPrefs($xProfile->user_id, $userPrefs);
944
945 return [
946 'prefs' => $userPrefs,
947 'message' => __('Email Notification preferences have been updated', 'fluent-community')
948 ];
949 }
950
951 public function reconfirmEmail(Request $request, $userName)
952 {
953 if (!defined('FLUENTCRM')) {
954 return $this->sendError([
955 'message' => __('FluentCRM is not available on this site', 'fluent-community')
956 ]);
957 }
958
959 $xProfile = XProfile::where('username', $userName)->firstOrFail();
960
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)) {
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'));
1018 }
1019
1020 return $xProfile;
1021 }
1022 }
1023