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 / Modules / Auth / AuthModdule.php

AuthModdule.php in FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses trunk, at Modules/Auth/AuthModdule.php

914 lines 40.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3
4 namespace FluentCommunity\Modules\Auth;
5
6 use FluentAuth\App\Hooks\Handlers\CustomAuthHandler;
7 use FluentCommunity\App\App;
8 use FluentCommunity\App\Functions\Utility;
9 use FluentCommunity\App\Services\AuthenticationService;
10 use FluentCommunity\App\Models\BaseSpace;
11 use FluentCommunity\App\Models\User;
12 use FluentCommunity\App\Services\FeedsHelper;
13 use FluentCommunity\App\Services\Helper;
14 use FluentCommunity\App\Services\ProfileHelper;
15 use FluentCommunity\App\Vite;
16 use FluentCommunity\Framework\Support\Arr;
17 use FluentCommunity\Modules\Auth\Classes\Invitation;
18 use FluentCommunity\Modules\Auth\Classes\InvitationHandler;
19 use FluentCommunity\Modules\Auth\Classes\InvitationService;
20
21 class AuthModdule
22 {
23 public function register($app)
24 {
25 add_action('fluent_community/portal_action_signed_url', [$this, 'maybeAutoLogin'], 10, 1);
26 add_action('fluent_community/portal_action_auth', [$this, 'viewAuthPage']);
27 add_action('wp_ajax_nopriv_fcom_user_registration', [$this, 'handleUserSignup']);
28 add_action('wp_ajax_fcom_user_registration', [$this, 'handleUserSignup']);
29 add_action('wp_ajax_nopriv_fcom_user_login_form', [$this, 'handleUserLogin']);
30 add_action('wp_ajax_fcom_user_login_form', [$this, 'handleUserLogin']);
31
32 add_filter('fluent_auth/login_redirect_url', function ($redirectUrl, $user) {
33 if (empty($_REQUEST['is_fcom_auth']) || empty($_REQUEST['fcom_redirect'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
34 return $redirectUrl;
35 }
36
37 // validate the url
38 $redirectUrl = wp_validate_redirect(sanitize_url(wp_unslash($_REQUEST['fcom_redirect'])), Helper::baseUrl()); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
39
40 $redirectUrl = apply_filters('fluent_community/auth/after_login_redirect_url', $redirectUrl, $user);
41 return $redirectUrl;
42 }, 10, 2);
43 }
44
45 public function maybeAutoLogin($requestData)
46 {
47 $urlHash = Arr::get($requestData, 'fcom_url_hash');
48 if ($urlHash && !get_current_user_id()) {
49 $tagetUser = ProfileHelper::getUserByUrlHash($urlHash);
50 if ($tagetUser) {
51 $willAtoLogin = apply_filters('fluent_community/allow_auto_login_by_url', !user_can($tagetUser, 'delete_pages'), $tagetUser);
52 if ($willAtoLogin) {
53 try {
54 InvitationService::makeLogin($tagetUser);
55 } catch (\Throwable $e) {
56 if (defined('WP_DEBUG') && WP_DEBUG) {
57 error_log('FluentCommunity: Auto-login failed for user #' . $tagetUser->ID . ': ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
58 }
59 }
60 }
61 }
62 }
63
64 // Remove fcom_action and fcom_url_hash from the current url
65 $currentUrl = home_url(add_query_arg($_GET, $GLOBALS['wp']->request)); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
66 $url = remove_query_arg(['fcom_action', 'fcom_url_hash'], $currentUrl);
67 $this->redirectAndExit($url);
68 }
69
70 /**
71 * Send a redirect and stop.
72 *
73 * Extracted only so it can be observed: a bare `exit()` terminates the PHP
74 * process, which in a test run kills the whole suite with no result (see
75 * FIX-PLAN item 22 for the same problem on PortalHandler). A test subclass
76 * overrides this and the two methods below to record what was about to
77 * happen and throw instead. Behaviour in production is unchanged — this is
78 * the original call, moved.
79 *
80 * This one keeps the UNSAFE variant its single caller already used. That
81 * caller builds its target with home_url(), so it is same-host by
82 * construction rather than by validation. Kept as a separate method from
83 * safeRedirectAndExit(), rather than a $safe flag, so the distinction stays
84 * visible to anyone grepping for wp_redirect.
85 */
86 protected function redirectAndExit($url, $status = 302)
87 {
88 wp_redirect($url, $status); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect
89 exit();
90 }
91
92 /**
93 * Send a host-confined redirect and stop. See redirectAndExit().
94 */
95 protected function safeRedirectAndExit($url)
96 {
97 wp_safe_redirect($url);
98 exit();
99 }
100
101 /**
102 * Render the headless page and stop. See redirectAndExit().
103 */
104 protected function renderPageAndExit($template, $pageVars)
105 {
106 status_header(200);
107 App::make('view')->render($template, $pageVars);
108 exit(200);
109 }
110
111 public function viewAuthPage()
112 {
113
114 add_filter('login_form_defaults', function ($defaults) {
115 $defaults['label_username'] = __('Email Address', 'fluent-community');
116 return $defaults;
117 });
118
119 add_filter('fluent_community/has_color_scheme', '__return_false');
120
121 $currentUserId = get_current_user_id();
122 // check if there has any invitation token
123 $inivtationToken = Arr::get($_GET, 'invitation_token'); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
124
125 $inviation = null;
126 if ($inivtationToken) {
127 $inviation = apply_filters('fluent_community/auth/invitation', null, $inivtationToken);
128 if ($inviation && !$inviation->isValid()) {
129 $inviation = null;
130 }
131 }
132
133 if ($currentUserId && !$inviation) {
134 $redirectUrl = null;
135 if (!empty($_REQUEST['redirect_to'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
136 $redirectUrl = sanitize_url(wp_unslash($_REQUEST['redirect_to'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
137 }
138 if (!$redirectUrl) {
139 $redirectUrl = Helper::baseUrl();
140 }
141
142 $this->safeRedirectAndExit($redirectUrl);
143 }
144
145 if ($currentUserId && $inviation) {
146 /** @var BaseSpace|null $space */
147 $space = BaseSpace::withoutGlobalScopes()->find($inviation->post_id);
148 if ($space) {
149 if (Helper::isUserInSpace($currentUserId, $inviation->post_id)) {
150 // let's redirect the user to the space
151 $redirectUrl = $space->getPermalink();
152 $this->safeRedirectAndExit($redirectUrl);
153 }
154
155 if (!empty($_REQUEST['auto_accept'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
156 $redirectUrl = (new InvitationHandler())->handleInvitationLogin(Helper::baseUrl(), get_user_by('ID', $currentUserId), $inviation->message_rendered);
157 if (is_wp_error($redirectUrl) || !$redirectUrl) {
158 $redirectUrl = Helper::baseUrl();
159 }
160 $this->safeRedirectAndExit($redirectUrl);
161 }
162 }
163 }
164
165 do_action('fluent_community/auth/before_auth_page_process', $currentUserId, $inviation);
166
167 $acceptedForms = ['login', 'register', 'reset_password'];
168 $targetForm = Arr::get($_GET, 'form'); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
169 $explicitForm = in_array($targetForm, $acceptedForms, true);
170 if (!$explicitForm) {
171 $targetForm = 'login';
172 }
173
174 if ($inviation && !$explicitForm) {
175 if ($inviation->message) {
176 $isUserAvailable = get_user_by('email', $inviation->message);
177 $targetForm = $isUserAvailable ? 'login' : 'register';
178 } else {
179 $targetForm = 'register';
180 }
181 }
182
183 if ($inviation && $currentUserId && $inviation->isValid()) {
184 if ($inviation->message) {
185 $invitedUser = get_user_by('email', $inviation->message);
186 if ($invitedUser && $invitedUser->ID == $currentUserId) {
187 $targetForm = 'accept_invitation';
188 }
189 } else {
190 $targetForm = 'accept_invitation';
191 }
192 }
193
194 $isFluentAuth = AuthHelper::isFluentAuthAvailable();
195 if (!$isFluentAuth && $targetForm == 'reset_password') {
196 $this->safeRedirectAndExit(wp_lostpassword_url(Helper::baseUrl()));
197 }
198
199 $portalSettings = Helper::generalSettings();
200 $titleVar = Arr::get($portalSettings, 'site_title');
201
202 $frameData = [
203 'logo' => Arr::get($portalSettings, 'logo', ''),
204 /* translators: %s is replaced by the title of the site */
205 'title' => sprintf(__('Join %s', 'fluent-community'), $titleVar),
206 'description' => __('Login or Signup to join the community', 'fluent-community'),
207 'button_label' => __('Login', 'fluent-community'),
208 ];
209
210 if ($targetForm == 'register') {
211 $frameData['button_label'] = __('Signup', 'fluent-community');
212 if (!$inviation) {
213 $customSignupUrl = Arr::get($portalSettings, 'custom_signup_url');
214 if ($customSignupUrl) {
215 $this->safeRedirectAndExit($customSignupUrl);
216 }
217 }
218 }
219
220 $currentUrl = home_url(add_query_arg($_GET, $GLOBALS['wp']->request)); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
221
222 do_action('fluent_community/enqueue_global_assets', true);
223 add_action('wp_enqueue_scripts', function () use ($isFluentAuth, $targetForm, $inviation) {
224 wp_enqueue_style('fluent_auth_styles', Vite::getStaticSrcUrl('user_registration.css'), [], FLUENT_COMMUNITY_PLUGIN_VERSION);
225 if(!$isFluentAuth || $targetForm == 'register' || $inviation) {
226 wp_enqueue_script('fluent_auth_scripts', Vite::getStaticSrcUrl('user_registration.js'), [], FLUENT_COMMUNITY_PLUGIN_VERSION, true);
227 wp_localize_script('fluent_auth_scripts', 'fluentComRegistration', array(
228 'ajax_url' => admin_url('admin-ajax.php'),
229 'is_logged_in' => is_user_logged_in(),
230 'redirecting_text' => __('Redirecting...', 'fluent-community'),
231 'i18n' => [
232 'generic_error' => esc_html__('Something went wrong. Please try again later', 'fluent-community'),
233 'network_error' => esc_html__('Could not reach the server. Please check your connection and try again.', 'fluent-community'),
234 ]
235 ));
236 }
237 }, 10);
238
239 $pageVars = [
240 'title' => $frameData['title'],
241 'og_title' => $frameData['title'],
242 'description' => $frameData['description'],
243 'url' => $currentUrl,
244 'featured_image' => '',
245 'css_files' => [],
246 'js_files' => [],
247 'js_vars' => [],
248 'scope' => 'user_registration',
249 'layout' => 'signup',
250 'portal' => [
251 'logo' => Arr::get($portalSettings, 'logo', ''),
252 /* translators: %s is replaced by the title of the site */
253 'title' => \sprintf(__('Welcome to %s', 'fluent-community'), Arr::get($portalSettings, 'site_title')),
254 'description' => get_bloginfo('description')
255 ]
256 ];
257
258 if (Utility::isDev()) {
259 $pageVars['js_files'] = [
260 Vite::getStaticSrcUrl('public/js/user_registration.js')
261 ];
262 }
263
264 $formType = ($targetForm == 'register') ? 'signup' : 'login';
265
266 $formSettings = AuthenticationService::getFormattedAuthSettings($formType);
267
268 if ($formSettings) {
269 $pageVars['portal'] = Arr::get($formSettings, 'banner');
270 $pageVars['portal']['form'] = Arr::get($formSettings, 'form');
271 }
272
273 add_action('fluent_community/headless/content', function ($context) use ($targetForm, $currentUrl, $frameData, $inviation, $formSettings) {
274 $preContent = apply_filters('fluent_community/auth/pre_content', '', $context, $targetForm, $frameData);
275 if ($preContent) {
276 return;
277 }
278
279 if ($targetForm == 'login') {
280 $frameData['button_label'] = Arr::get($formSettings, 'form.button_label', __('Login', 'fluent-community'));
281 $this->showLoginForm($frameData, $inviation);
282 } else if ($targetForm == 'reset_password') {
283 $frameData['title'] = __('Reset your password', 'fluent-community');
284 ?>
285 <div id="fcom_user_onboard_wrap" class="fcom_user_onboard">
286 <div class="fcom_onboard_header">
287 <div class="fcom_onboard_header_title">
288 <h2><?php echo esc_html($frameData['title']); ?></h2>
289 </div>
290 <div class="fcom_onboard_sub">
291 <p><?php esc_html_e('Please enter your email address. You will receive an email message with instructions on how to reset your password.', 'fluent-community'); ?></p>
292 </div>
293 </div>
294 <div class="fcom_onboard_body">
295 <div class="fcom_onboard_form">
296 <?php echo do_shortcode('[fluent_auth_reset_password redirect_to="' . esc_url($currentUrl) . '"]'); ?>
297 <div class="fcom_spaced_divider">
298 <div class="fcom_alt_auth_text">
299 <a href="<?php echo esc_url(add_query_arg('form', 'login', $currentUrl)); ?>">
300 <?php esc_html_e('Back to Login', 'fluent-community'); ?>
301 </a>
302 </div>
303 </div>
304 </div>
305 </div>
306 </div>
307 <?php
308 } else if ($targetForm == 'accept_invitation') {
309 do_action('fluent_community/auth/show_invitation_for_user', $inviation, $frameData);
310 } else {
311 //check if the registration is disabled (a valid invitation still allows signup)
312 if (!$inviation && !AuthHelper::isRegistrationEnabled()) {
313 echo '<div class="fcom_completed"><div class="fcom_complted_header"><h4>' . esc_html__('Registration is disabled for this community', 'fluent-community') . '</h4>';
314 return;
315 }
316
317 $frameData['hiddenFields'] = [
318 'register' => 'yes',
319 'action' => 'fcom_user_signup',
320 '_fcom_signup_nonce' => wp_create_nonce('fluent_auth_signup_nonce')
321 ];
322
323 $frameData['loginUrl'] = add_query_arg('form', 'login', $currentUrl);
324 $frameData = wp_parse_args(Arr::get($formSettings, 'form'), $frameData);
325
326 $this->renderRegistrationForm($frameData, $inviation);
327 }
328 }, 10, 1);
329
330 add_action('fluent_community/headless/head_early', function ($scope) use ($formSettings) {
331 $bannerColors = array_filter(Arr::only($formSettings['banner'], ['title_color', 'text_color', 'background_color']));
332 $css = Utility::getColorCssVariables();
333
334 $sideVars = '';
335 foreach ($bannerColors as $colorKey => $colorValue) {
336 $sideVars .= '--fcom_' . $colorKey . ': ' . $colorValue . ';';
337 }
338 ?>
339 <?php // the auth screen renders with load_wp, which skips headless_page's noindex ?>
340 <meta name="robots" content="noindex, noarchive" />
341 <link rel="canonical" href="<?php echo esc_url(Helper::getAuthUrl()); ?>" />
342 <style>
343 .fcom_layout_side { <?php echo esc_html($sideVars); ?> }
344 <?php echo esc_html($css); ?>
345 </style>
346 <?php
347 });
348
349 $pageVars['load_wp'] = 'yes';
350
351 // document title hook
352 add_filter('pre_get_document_title', function ($title) use ($frameData) {
353 return $frameData['title'];
354 }, 9999, 1);
355
356 $this->renderPageAndExit('headless_page', $pageVars);
357 }
358
359 public function handleUserSignup()
360 {
361 if (is_user_logged_in()) {
362 return $this->handleSignupCompleted(get_current_user_id());
363 }
364
365 $signupNonce = isset($_POST['_fcom_signup_nonce']) ? sanitize_text_field(wp_unslash($_POST['_fcom_signup_nonce'])) : '';
366 if (!$signupNonce || !wp_verify_nonce($signupNonce, 'fluent_auth_signup_nonce')) {
367 wp_send_json([
368 'message' => esc_html__('Invalid request. Please refresh the page and try again.', 'fluent-community')
369 ], 403);
370 }
371
372 $invitationToken = isset($_POST['invitation_token']) ? sanitize_text_field(wp_unslash($_POST['invitation_token'])) : '';
373 $hasValidInvitation = false;
374 if ($invitationToken) {
375 $pendingInvitation = Invitation::where('message_rendered', $invitationToken)->first();
376 $hasValidInvitation = $pendingInvitation && $pendingInvitation->isValid();
377 }
378
379 // A valid invitation must still allow signup even when public registration is disabled.
380 if (!$hasValidInvitation && !AuthHelper::isRegistrationEnabled()) {
381 wp_send_json([
382 'message' => esc_html__('Registration is disabled for this community', 'fluent-community')
383 ], 422);
384 }
385
386 $app = App::make('app');
387 $request = $app->make('request');
388 $fields = AuthHelper::getFormFields();
389
390 $authSettings = AuthenticationService::getAuthSettings();
391 $termsField = Arr::get($authSettings, 'signup.form.fields.terms');
392
393 $fields['terms'] = $termsField ?: $fields['terms'];
394
395 $requiredFields = array_filter($fields, function ($field) {
396 return ($field['required'] && empty($field['disabled'])) ?? false;
397 });
398
399 $keys = array_keys($fields);
400 $data = Arr::only($request->all(), $keys);
401
402 // remove space and special characters from username
403 $data['username'] = sanitize_user(strtolower(preg_replace('/[^A-Za-z0-9_]/', '', $data['username'])));
404
405 if (empty($data['username'])) {
406 wp_send_json([
407 'message' => esc_html__('Username is not valid', 'fluent-community'),
408 'errors' => [
409 'username' => __('Please provide a valid username', 'fluent-community')
410 ]
411 ], 422);
412 }
413
414 if (!ProfileHelper::isUsernameAvailable($data['username'])) {
415 wp_send_json([
416 'message' => esc_html__('Username is already taken', 'fluent-community'),
417 'errors' => [
418 'username' => __('Username is already taken. Please use a different username', 'fluent-community')
419 ]
420 ], 422);
421 }
422
423 $invitationToken = $request->get('invitation_token');
424 $invitation = null;
425 if ($invitationToken) {
426 $invitation = Invitation::where('message_rendered', $invitationToken)->first();
427 if (!$invitation) {
428 wp_send_json([
429 'message' => __('Invalid invitation token', 'fluent-community')
430 ], 422);
431 }
432
433 if ($invitation->message && $invitation->message != $data['email']) {
434 wp_send_json([
435 'message' => esc_html__('Email does not match with the invitation', 'fluent-community')
436 ], 422);
437 }
438
439 if ($invitation->message) {
440 add_filter('fluent_community/auth/two_factor_enabled', '__return_false');
441 add_filter('fluent_auth/verify_signup_email', '__return_false');
442 }
443 }
444
445 if (!$invitation && AuthenticationService::getCustomSignupPageUrl()) {
446 // we have custom signup page enabled
447 wp_send_json([
448 'message' => esc_html__('Direct Registration is disabled for this community', 'fluent-community')
449 ], 422);
450 }
451
452 $data['email'] = sanitize_email($data['email']);
453
454 $validations = [
455 'full_name' => 'required|max:100|string',
456 'username' => 'required|unique:users,user_login|unique:fcom_xprofile,username|min:4|max:30',
457 'email' => 'required|email|unique:users,user_email',
458 'password' => 'required|same:conf_password|max:50|string',
459 'conf_password' => 'required|same:password'
460 ];
461
462 if (!AuthHelper::isPasswordConfRequired()) {
463 unset($validations['conf_password']);
464 $validations['password'] = 'required|max:50|string';
465 }
466
467 foreach ($requiredFields as $key => $field) {
468 if (!isset($data[$key])) {
469 $validations[$key] = 'required';
470 }
471 }
472
473 $validator = $app->make('validator')->make($data, $validations, [
474 'username.required' => __('Username is required', 'fluent-community'),
475 'username.unique' => __('Username is already taken', 'fluent-community'),
476 'email.required' => __('Email is required', 'fluent-community'),
477 'email.email' => __('Email is not valid', 'fluent-community'),
478 'email.unique' => __('Email is already taken', 'fluent-community'),
479 'password.required' => __('Password is required', 'fluent-community'),
480 'password.same' => __('Password and confirmation password do not match', 'fluent-community'),
481 'conf_password.required' => __('Password confirmation is required', 'fluent-community'),
482 'conf_password.same' => __('Password and confirmation password do not match', 'fluent-community'),
483 'terms.required' => __('You must agree to the terms and conditions', 'fluent-community'),
484 'full_name.required' => __('Full name is required', 'fluent-community'),
485 ]);
486
487 if ($validator->fails()) {
488 wp_send_json([
489 'message' => __('Please fill in all required fields correctly.', 'fluent-community'),
490 'errors' => $validator->errors()
491 ], 422);
492 }
493
494 foreach ($data as $key => $value) {
495 // let's sanitize the data
496 $callBack = $fields[$key]['sanitize_callback'] ?? null;
497 if ($callBack) {
498 $data[$key] = call_user_func($callBack, $value);
499 }
500 }
501
502 // let's extract the full_name and set the first_name and last_name
503 if (!empty($data['full_name'])) {
504 $nameParts = explode(' ', $data['full_name']);
505 $data['first_name'] = $nameParts[0];
506 $data['last_name'] = implode(' ', array_slice($nameParts, 1));
507 unset($data['full_name']);
508 $data = array_filter($data);
509 }
510
511 $rateLimit = AuthHelper::isAuthRateLimit();
512
513 if (is_wp_error($rateLimit)) {
514 wp_send_json([
515 'message' => $rateLimit->get_error_message()
516 ], 422);
517 }
518
519 // We need two-factor authentication here
520 if (AuthHelper::isTwoFactorEnabled()) {
521 // Check if Two Factor code is given
522 $verificationToken = $request->get('__two_fa_signed_token');
523 if ($verificationToken) {
524 $code = $request->get('_email_verification_code');
525 if (!$code) {
526 wp_send_json([
527 'message' => __('Verification code is required', 'fluent-community')
528 ], 422);
529 }
530
531 $validated = AuthHelper::validateVerificationCode($code, $verificationToken, $data);
532 if (is_wp_error($validated)) {
533 wp_send_json([
534 'message' => $validated->get_error_message()
535 ], 422);
536 }
537 } else {
538 // Let's send the verification code
539 $htmlForm = AuthHelper::get2FaRegistrationCodeForm($data);
540 wp_send_json([
541 'verifcation_html' => $htmlForm
542 ]);
543 }
544 }
545
546 // let's create the user now
547 $userId = AuthHelper::registerNewUser($data['username'], $data['email'], $data['password'], [
548 'first_name' => Arr::get($data, 'first_name'),
549 'last_name' => Arr::get($data, 'last_name'),
550 'role' => get_option('default_role', 'subscriber')
551 ]);
552
553 if (is_wp_error($userId)) {
554 wp_send_json([
555 'message' => $userId->get_error_message()
556 ], 422);
557 }
558
559 $this->handleSignupCompleted($userId);
560 }
561
562 private function handleSignupViaFlentAuth($data)
563 {
564 add_action('fluent_auth/after_creating_user', function ($userId) {
565 $this->handleSignupCompleted($userId);
566 }, 1, 1);
567
568 add_filter('fluent_auth/signup_enabled', '__return_true');
569
570 (new CustomAuthHandler())->handleSignupAjax();
571 }
572
573 private function handleSignupCompleted($userId)
574 {
575 // We have the user now let's set the community membership
576 $user = User::find($userId);
577 $user->syncXProfile(true, true);
578
579 $redirectUrl = Helper::baseUrl();
580
581 if (!empty($_REQUEST['redirect_to'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
582 $redirectUrl = wp_validate_redirect(sanitize_url(wp_unslash($_REQUEST['redirect_to'])), Helper::baseUrl()); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
583 }
584
585 $redirectUrl = apply_filters('fluent_community/auth/after_signup_redirect_url', $redirectUrl, $user, $_REQUEST); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
586 $btnText = __('Continue to the community', 'fluent-community');
587
588 $html = '<div class="fcom_completed"><div class="fcom_complted_header"><h2>' . __('Congratulations!', 'fluent-community') . '</h2>';
589 $html .= '<p>' . __('You have successfully registered to the community', 'fluent-community') . '</p></div>';
590 $html .= '<a href="' . esc_url($redirectUrl) . '" class="fcom_btn fcom_btn_success">' . $btnText . '</a>';
591 $html .= '</div>';
592
593 if (!get_current_user_id()) {
594 $wpUser = get_user_by('ID', $userId);
595 AuthHelper::makeLogin($wpUser);
596 }
597
598 wp_send_json([
599 'success_html' => $html,
600 'redirect_url' => $redirectUrl
601 ]);
602 }
603
604 public function handleUserLogin()
605 {
606 if (is_user_logged_in()) {
607 $user = get_user_by('ID', get_current_user_id());
608 return $this->handleUserLoginSuccess($user);
609 }
610
611 if (AuthHelper::isFluentAuthAvailable()) {
612 wp_send_json([
613 'message' => __('This form cannot be used to log in. Please reload the page and try again.', 'fluent-community')
614 ], 422);
615 }
616
617 $loginNonce = isset($_POST['_fcom_login_nonce']) ? sanitize_text_field(wp_unslash($_POST['_fcom_login_nonce'])) : '';
618 if (!$loginNonce || !wp_verify_nonce($loginNonce, 'fcom_user_login_nonce')) {
619 wp_send_json([
620 'message' => esc_html__('Invalid request. Please refresh the page and try again.', 'fluent-community')
621 ], 403);
622 }
623
624 $app = App::make('app');
625 $request = $app->make('request');
626
627 $data = $request->all();
628
629 $validator = $app->make('validator')->make($data, [
630 'log' => 'required',
631 'pwd' => 'required'
632 ], [
633 'log.required' => __('Email is required', 'fluent-community'),
634 'pwd.required' => __('Password is required', 'fluent-community')
635 ]);
636
637 if ($validator->fails()) {
638 wp_send_json([
639 'message' => __('Please fill all the required fields correctly', 'fluent-community'),
640 'errors' => $validator->errors()
641 ], 422);
642 }
643
644 $rateLimit = AuthHelper::isAuthRateLimit();
645 if (is_wp_error($rateLimit)) {
646 wp_send_json([
647 'message' => $rateLimit->get_error_message()
648 ], 422);
649 }
650
651 $user = wp_authenticate($data['log'], $data['pwd']);
652
653 if (is_wp_error($user)) {
654 $enumerationCodes = ['invalid_username', 'invalid_email', 'incorrect_password'];
655 if (in_array($user->get_error_code(), $enumerationCodes, true)) {
656 $message = __('Email or password is incorrect.', 'fluent-community');
657 } else {
658 $message = $user->get_error_message();
659 }
660 wp_send_json([
661 'message' => $message
662 ], 422);
663 }
664
665 InvitationService::makeLogin($user);
666
667 $redirectUrl = null;
668 if (!empty($_REQUEST['redirect_to'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
669 $redirectUrl = wp_validate_redirect(sanitize_url(wp_unslash($_REQUEST['redirect_to'])), Helper::baseUrl()); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
670 }
671
672 if (!$redirectUrl) {
673 $redirectUrl = Helper::baseUrl();
674 }
675
676 if ($invitationToken = $request->get('invitation_token')) {
677 $maybeRedirectUrl = apply_filters('fluent_community/auth/after_login_with_invitation', null, $user, $invitationToken);
678 if ($maybeRedirectUrl && !is_wp_error($maybeRedirectUrl)) {
679 $redirectUrl = $maybeRedirectUrl;
680 }
681 }
682
683 $this->handleUserLoginSuccess($user, $redirectUrl);
684 }
685
686 private function handleUserLoginSuccess($user, $redirectUrl = null)
687 {
688 if (!$redirectUrl) {
689 $redirectUrl = Helper::baseUrl();
690 }
691
692 $redirectUrl = apply_filters('fluent_community/auth/after_login_redirect_url', $redirectUrl, $user);
693 $btnText = __('Continue to the community', 'fluent-community');
694
695 $html = '<div class="fcom_completed"><div class="fcom_complted_header"><h2>' . __('Welcome back!', 'fluent-community') . '</h2>';
696 $html .= '<p>' . __('You have successfully logged in to the community', 'fluent-community') . '</p></div>';
697 $html .= '<a href="' . esc_url($redirectUrl) . '" class="fcom_btn fcom_btn_success">' . $btnText . '</a>';
698 $html .= '</div>';
699
700 wp_send_json([
701 'success_html' => $html,
702 'redirect_url' => $redirectUrl
703 ]);
704 }
705
706 public function showLoginForm($frameData, $invitation = null)
707 {
708 $portalSettings = Helper::generalSettings();
709 $isFluentAuth = AuthHelper::isFluentAuthAvailable();
710 $loginSettings = AuthenticationService::getFormattedAuthSettings('login');
711 $formSettings = Arr::get($loginSettings, 'form');
712 $currentUrl = home_url(add_query_arg($_GET, $GLOBALS['wp']->request)); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
713 /* translators: %s is replaced by the title of the site */
714 $title = sprintf(__('Login to %s', 'fluent-community'), Arr::get($portalSettings, 'site_title'));
715
716 $description = '';
717 if ($invitation) {
718 $invitationBy = $invitation->xprofile ? $invitation->xprofile->display_name : __('Someone', 'fluent-community');
719 if ($invitation->post_id) {
720 $space = BaseSpace::find($invitation->post_id);
721 if ($space) {
722 $title = $space->title . ' - ' . Arr::get($portalSettings, 'site_title');
723 }
724 }
725 /* translators: %s is replaced by the name of the inviter */
726 $inviteDescription = \sprintf(__('%s has invited you to join this community. Please login to accept your invitation.', 'fluent-community'), $invitationBy);
727 add_action('fluent_community/before_auth_form_header', function ($formType) use ($inviteDescription) {
728 ?>
729 <div class="fcom_highlight_message">
730 <?php echo wp_kses_post($inviteDescription); ?>
731 </div>
732 <?php
733 });
734 }
735
736 $signupUrl = add_query_arg('form', 'register', $currentUrl);
737
738 if (!$invitation) {
739 if ($customSignupUrl = AuthenticationService::getCustomSignupPageUrl()) {
740 $signupUrl = $customSignupUrl;
741 }
742 }
743
744 add_filter('login_form_defaults', function ($defaults) use ($invitation, $frameData) {
745 $defaults['label_log_in'] = Arr::get($frameData, 'button_label');
746 return $defaults;
747 });
748
749 if ($isFluentAuth) {
750 add_filter('login_form_top', function () use ($invitation) {
751 $reditectUrl = Arr::get($_GET, 'redirect_to'); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
752 if (!$reditectUrl) {
753 $reditectUrl = apply_filters('fluent_community/default_redirect_url', Helper::baseUrl());
754 }
755 ob_start();
756 ?>
757 <?php if ($invitation) { ?>
758 <input type="hidden" name="invitation_token" value="<?php echo esc_attr($invitation->message_rendered); ?>"/>
759 <?php } ?>
760 <input name="is_fcom_auth" type="hidden" value="yes"/>
761 <input type="hidden" name="fcom_redirect" value="<?php echo esc_url($reditectUrl); ?>"/>
762 <?php
763 return ob_get_clean();
764 });
765 ?>
766 <div id="fcom_user_onboard_wrap" class="fcom_user_onboard">
767 <div class="fcom_onboard_header">
768 <?php do_action('fluent_community/before_auth_form_header', 'login'); ?>
769 <div class="fcom_onboard_header_title">
770 <?php if (!empty($formSettings['title'])): ?>
771 <h2>
772 <?php echo esc_html($formSettings['title']); ?>
773 </h2>
774 <?php endif; ?>
775 </div>
776 <?php if (!empty($formSettings['description'])): ?>
777 <div class="fcom_onboard_sub">
778 <?php echo wp_kses_post(trim($formSettings['description'])); ?>
779 </div>
780 <?php endif; ?>
781 </div>
782 <div class="fcom_onboard_body">
783 <div class="fcom_onboard_form">
784 <?php echo do_shortcode('[fluent_auth_login redirect_to="' . esc_url($currentUrl) . '"]'); ?>
785 <div class="fcom_spaced_divider">
786 <?php if ($invitation || AuthHelper::isRegistrationEnabled()): ?>
787 <div class="fcom_alt_auth_text">
788 <?php esc_html_e('Don\'t have an account?', 'fluent-community'); ?>
789 <a href="<?php echo esc_url($signupUrl); ?>">
790 <?php esc_html_e('Signup', 'fluent-community'); ?>
791 </a>
792 </div>
793 <?php endif; ?>
794 <p class="fcom_reset_pass_text">
795 <a href="<?php echo esc_url(AuthHelper::getLostPasswordUrl($currentUrl)); ?>">
796 <?php esc_html_e('Lost your password?', 'fluent-community'); ?>
797 </a>
798 </p>
799 </div>
800 </div>
801 </div>
802 </div>
803 <?php
804 return;
805 }
806
807 $frameData['redirect'] = $currentUrl;
808
809 $frameData['hiddenFields'] = [
810 'action' => 'fcom_user_login_form',
811 '_fcom_login_nonce' => wp_create_nonce('fcom_user_login_nonce'),
812 ];
813 if ($invitation) {
814 $frameData['button_label'] = __('Log In & Accept Invitation', 'fluent-community');
815 $frameData['hiddenFields']['invitation_token'] = $invitation->message_rendered;
816 }
817
818 $frameData['title'] = $title;
819 $frameData['description'] = $description;
820
821 $frameData['defaults'] = [
822 'email' => $invitation ? $invitation->message : ''
823 ];
824
825 if ($invitation || AuthHelper::isRegistrationEnabled()) {
826 $frameData['signupUrl'] = $signupUrl;
827 }
828
829 $frameData['settings'] = $formSettings;
830
831 if (isset($_GET['redirect_to'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
832 $frameData['redirect_to'] = sanitize_url(wp_unslash($_GET['redirect_to'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
833 }
834
835 App::make('view')->render('auth.login_form', $frameData);
836 }
837
838 public function renderRegistrationForm($frameData, $invitation = null)
839 {
840 $formFields = AuthHelper::getFormFields($invitation);
841
842 // Prefill the name from the invitation link's query param when present.
843 $inviteName = isset($_GET['invite_name']) ? sanitize_text_field(wp_unslash($_GET['invite_name'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
844 if ($inviteName && isset($formFields['full_name']) && empty($formFields['full_name']['value'])) {
845 $formFields['full_name']['value'] = $inviteName;
846 }
847
848 $authSettings = AuthenticationService::getAuthSettings();
849
850 $termsField = Arr::get($authSettings, 'signup.form.fields.terms');
851
852
853 if ($termsField) {
854 unset($termsField['label']);
855
856 // add new tab on the link for $termsField['inline_label']
857 $termsField['inline_label'] = FeedsHelper::addNewTabToLinks($termsField['inline_label']);
858
859 $formFields['terms'] = $termsField;
860 }
861
862 $frameData['formFields'] = $formFields;
863
864 if ($invitation) {
865 $frameData['hiddenFields'] = [
866 'invitation_token' => $invitation->message_rendered,
867 'action' => 'fcom_user_registration',
868 '_fcom_signup_nonce' => wp_create_nonce('fluent_auth_signup_nonce')
869 ];
870
871 $invitationBy = $invitation->xprofile ? $invitation->xprofile->display_name : __('Someone', 'fluent-community');
872 /* translators: %s is replaced by the name of the inviter */
873 $inviteDescription = sprintf(__('%s has invited you to join this community. Please create an account to accept your invitation.', 'fluent-community'), $invitationBy);
874
875 add_action('fluent_community/before_auth_form_header', function ($formType) use ($inviteDescription) {
876 ?>
877 <div class="fcom_highlight_message">
878 <?php echo wp_kses_post($inviteDescription); ?>
879 </div>
880 <?php
881 });
882
883 $frameData['button_label'] = __('Register & Accept invitation', 'fluent-community');
884 } else {
885 $frameData['hiddenFields'] = [
886 'register' => 'yes',
887 'action' => 'fcom_user_registration',
888 '_fcom_signup_nonce' => wp_create_nonce('fluent_auth_signup_nonce'),
889 ];
890
891 if (!empty($_GET['redirect_to'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
892 $frameData['hiddenFields']['redirect_to'] = sanitize_url(wp_unslash($_GET['redirect_to'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
893 }
894 }
895
896 add_action('fluent_community/before_registration_form', function ($frameData) {
897 if (AuthHelper::isFluentAuthAvailable()) {
898 $currentUrl = esc_url(home_url(add_query_arg($_GET, $GLOBALS['wp']->request))); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
899
900 $titlePrefix = __('Signup with', 'fluent-community');
901 $html = do_shortcode('[fs_auth_buttons redirect="' . $currentUrl . '" title_prefix="' . $titlePrefix . ' " title=""]');
902
903 if ($html) {
904 echo '<div class="fcom_social_auth_wrap">';
905 echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
906 echo '</div>';
907 }
908 }
909 });
910
911 App::make('view')->render('auth.user_invitation', $frameData);
912 }
913 }
914