PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.11.0
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.11.0
2.11.0 2.10.0 2.10.01 2.9.1 2.9.0 2.8.1 2.8.0 2.7.7 2.7.5 2.7.0 2.6.01 2.6.0 2.5.0 2.4.01 trunk 1.0.90 1.0.91 1.0.92 1.0.93 1.0.94 1.0.95 1.0.96 1.0.97 1.0.98 1.0.99 All 78 releases
fluent-community / Modules / Auth / AuthModdule.php

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

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