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
← All changes | Modules/Auth/AuthModdule.php +127 -39 2.4.012.11.0 View file →
@@ -28,8 +28,19 @@
28 28 add_action('wp_ajax_fcom_user_registration', [$this, 'handleUserSignup']);
29 29 add_action('wp_ajax_nopriv_fcom_user_login_form', [$this, 'handleUserLogin']);
30 30 add_action('wp_ajax_fcom_user_login_form', [$this, 'handleUserLogin']);
31 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 +
32 43 add_filter('fluent_auth/login_redirect_url', function ($redirectUrl, $user) {
33 44 if (empty($_REQUEST['is_fcom_auth']) || empty($_REQUEST['fcom_redirect'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
34 45 return $redirectUrl;
35 46 }
@@ -52,9 +63,11 @@
52 63 if ($willAtoLogin) {
53 64 try {
54 65 InvitationService::makeLogin($tagetUser);
55 66 } catch (\Throwable $e) {
56 - error_log('FluentCommunity: Auto-login failed for user #' . $tagetUser->ID . ': ' . $e->getMessage());
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 + }
57 70 }
58 71 }
59 72 }
60 73 }
@@ -61,12 +74,52 @@
61 74
62 75 // Remove fcom_action and fcom_url_hash from the current url
63 76 $currentUrl = home_url(add_query_arg($_GET, $GLOBALS['wp']->request)); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
64 77 $url = remove_query_arg(['fcom_action', 'fcom_url_hash'], $currentUrl);
65 - wp_redirect($url, 302); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect
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
66 100 exit();
67 101 }
68 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 +
69 122 public function viewAuthPage()
70 123 {
71 124
72 125 add_filter('login_form_defaults', function ($defaults) {
@@ -96,20 +149,19 @@
96 149 if (!$redirectUrl) {
97 150 $redirectUrl = Helper::baseUrl();
98 151 }
99 152
100 - wp_safe_redirect($redirectUrl);
101 - exit();
153 + $this->safeRedirectAndExit($redirectUrl);
102 154 }
103 155
104 156 if ($currentUserId && $inviation) {
157 + /** @var BaseSpace|null $space */
105 158 $space = BaseSpace::withoutGlobalScopes()->find($inviation->post_id);
106 159 if ($space) {
107 160 if (Helper::isUserInSpace($currentUserId, $inviation->post_id)) {
108 161 // let's redirect the user to the space
109 162 $redirectUrl = $space->getPermalink();
110 - wp_safe_redirect($redirectUrl);
111 - exit();
163 + $this->safeRedirectAndExit($redirectUrl);
112 164 }
113 165
114 166 if (!empty($_REQUEST['auto_accept'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
115 167 $redirectUrl = (new InvitationHandler())->handleInvitationLogin(Helper::baseUrl(), get_user_by('ID', $currentUserId), $inviation->message_rendered);
@@ -115,10 +167,9 @@
115 167 $redirectUrl = (new InvitationHandler())->handleInvitationLogin(Helper::baseUrl(), get_user_by('ID', $currentUserId), $inviation->message_rendered);
116 168 if (is_wp_error($redirectUrl) || !$redirectUrl) {
117 169 $redirectUrl = Helper::baseUrl();
118 170 }
119 - wp_safe_redirect($redirectUrl);
120 - exit();
171 + $this->safeRedirectAndExit($redirectUrl);
121 172 }
122 173 }
123 174 }
124 175
@@ -125,16 +176,19 @@
125 176 do_action('fluent_community/auth/before_auth_page_process', $currentUserId, $inviation);
126 177
127 178 $acceptedForms = ['login', 'register', 'reset_password'];
128 179 $targetForm = Arr::get($_GET, 'form'); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
129 - if (!in_array($targetForm, $acceptedForms)) {
180 + $explicitForm = in_array($targetForm, $acceptedForms, true);
181 + if (!$explicitForm) {
130 182 $targetForm = 'login';
131 183 }
132 184
133 - if ($inviation && $targetForm != 'reset_password') {
185 + if ($inviation && !$explicitForm) {
134 186 if ($inviation->message) {
135 187 $isUserAvailable = get_user_by('email', $inviation->message);
136 188 $targetForm = $isUserAvailable ? 'login' : 'register';
189 + } else {
190 + $targetForm = 'register';
137 191 }
138 192 }
139 193
140 194 if ($inviation && $currentUserId && $inviation->isValid()) {
@@ -147,12 +201,21 @@
147 201 $targetForm = 'accept_invitation';
148 202 }
149 203 }
150 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 +
151 215 $isFluentAuth = AuthHelper::isFluentAuthAvailable();
152 216 if (!$isFluentAuth && $targetForm == 'reset_password') {
153 - wp_safe_redirect(wp_lostpassword_url(Helper::baseUrl()));
154 - exit();
217 + $this->safeRedirectAndExit(wp_lostpassword_url(Helper::baseUrl()));
155 218 }
156 219
157 220 $portalSettings = Helper::generalSettings();
158 221 $titleVar = Arr::get($portalSettings, 'site_title');
@@ -169,10 +232,9 @@
169 232 $frameData['button_label'] = __('Signup', 'fluent-community');
170 233 if (!$inviation) {
171 234 $customSignupUrl = Arr::get($portalSettings, 'custom_signup_url');
172 235 if ($customSignupUrl) {
173 - wp_safe_redirect($customSignupUrl);
174 - exit();
236 + $this->safeRedirectAndExit($customSignupUrl);
175 237 }
176 238 }
177 239 }
178 240
@@ -185,9 +247,13 @@
185 247 wp_enqueue_script('fluent_auth_scripts', Vite::getStaticSrcUrl('user_registration.js'), [], FLUENT_COMMUNITY_PLUGIN_VERSION, true);
186 248 wp_localize_script('fluent_auth_scripts', 'fluentComRegistration', array(
187 249 'ajax_url' => admin_url('admin-ajax.php'),
188 250 'is_logged_in' => is_user_logged_in(),
189 - 'redirecting_text' => __('Redirecting...', 'fluent-community')
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 + ]
190 256 ));
191 257 }
192 258 }, 10);
193 259
@@ -262,10 +328,10 @@
262 328 <?php
263 329 } else if ($targetForm == 'accept_invitation') {
264 330 do_action('fluent_community/auth/show_invitation_for_user', $inviation, $frameData);
265 331 } else {
266 - //check if the registration is disabled
267 - if (!AuthHelper::isRegistrationEnabled()) {
332 + //check if the registration is disabled (a valid invitation still allows signup)
333 + if (!$inviation && !AuthHelper::isRegistrationEnabled()) {
268 334 echo '<div class="fcom_completed"><div class="fcom_complted_header"><h4>' . esc_html__('Registration is disabled for this community', 'fluent-community') . '</h4>';
269 335 return;
270 336 }
271 337
@@ -283,15 +349,20 @@
283 349 }, 10, 1);
284 350
285 351 add_action('fluent_community/headless/head_early', function ($scope) use ($formSettings) {
286 352 $bannerColors = array_filter(Arr::only($formSettings['banner'], ['title_color', 'text_color', 'background_color']));
287 - $css = Utility::getColorCssVariables(); ?>
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" />
288 362 <link rel="canonical" href="<?php echo esc_url(Helper::getAuthUrl()); ?>" />
289 363 <style>
290 - .fcom_layout_side {
291 - <?php foreach ($bannerColors as $colorKey => $colorValue): ?> --fcom_ <?php echo esc_html($colorKey); ?>: <?php echo esc_html($colorValue); ?>;
292 - <?php endforeach; ?>
293 - }
364 + .fcom_layout_side { <?php echo esc_html($sideVars); ?> }
294 365 <?php echo esc_html($css); ?>
295 366 </style>
296 367 <?php
297 368 });
@@ -302,11 +373,9 @@
302 373 add_filter('pre_get_document_title', function ($title) use ($frameData) {
303 374 return $frameData['title'];
304 375 }, 9999, 1);
305 376
306 - status_header(200);
307 - App::make('view')->render('headless_page', $pageVars);
308 - exit(200);
377 + $this->renderPageAndExit('headless_page', $pageVars);
309 378 }
310 379
311 380 public function handleUserSignup()
312 381 {
@@ -313,14 +382,8 @@
313 382 if (is_user_logged_in()) {
314 383 return $this->handleSignupCompleted(get_current_user_id());
315 384 }
316 385
317 - if (!AuthHelper::isRegistrationEnabled()) {
318 - wp_send_json([
319 - 'message' => esc_html__('Registration is disabled for this community', 'fluent-community')
320 - ], 422);
321 - }
322 -
323 386 $signupNonce = isset($_POST['_fcom_signup_nonce']) ? sanitize_text_field(wp_unslash($_POST['_fcom_signup_nonce'])) : '';
324 387 if (!$signupNonce || !wp_verify_nonce($signupNonce, 'fluent_auth_signup_nonce')) {
325 388 wp_send_json([
326 389 'message' => esc_html__('Invalid request. Please refresh the page and try again.', 'fluent-community')
@@ -326,8 +389,22 @@
326 389 'message' => esc_html__('Invalid request. Please refresh the page and try again.', 'fluent-community')
327 390 ], 403);
328 391 }
329 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 +
330 407 $app = App::make('app');
331 408 $request = $app->make('request');
332 409 $fields = AuthHelper::getFormFields();
333 410
@@ -393,8 +470,9 @@
393 470 ], 422);
394 471 }
395 472
396 473 $data['email'] = sanitize_email($data['email']);
474 + $data['full_name'] = sanitize_text_field(Arr::get($data, 'full_name', ''));
397 475
398 476 $validations = [
399 477 'full_name' => 'required|max:100|string',
400 478 'username' => 'required|unique:users,user_login|unique:fcom_xprofile,username|min:4|max:30',
@@ -522,9 +600,9 @@
522 600
523 601 $redirectUrl = Helper::baseUrl();
524 602
525 603 if (!empty($_REQUEST['redirect_to'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
526 - $redirectUrl = sanitize_url(wp_unslash($_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
527 605 }
528 606
529 607 $redirectUrl = apply_filters('fluent_community/auth/after_signup_redirect_url', $redirectUrl, $user, $_REQUEST); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
530 608 $btnText = __('Continue to the community', 'fluent-community');
@@ -594,10 +672,16 @@
594 672
595 673 $user = wp_authenticate($data['log'], $data['pwd']);
596 674
597 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 + }
598 682 wp_send_json([
599 - 'message' => $user->get_error_message()
683 + 'message' => $message
600 684 ], 422);
601 685 }
602 686
603 687 InvitationService::makeLogin($user);
@@ -720,9 +804,9 @@
720 804 <div class="fcom_onboard_body">
721 805 <div class="fcom_onboard_form">
722 806 <?php echo do_shortcode('[fluent_auth_login redirect_to="' . esc_url($currentUrl) . '"]'); ?>
723 807 <div class="fcom_spaced_divider">
724 - <?php if (AuthHelper::isRegistrationEnabled()): ?>
808 + <?php if ($invitation || AuthHelper::isRegistrationEnabled()): ?>
725 809 <div class="fcom_alt_auth_text">
726 810 <?php esc_html_e('Don\'t have an account?', 'fluent-community'); ?>
727 811 <a href="<?php echo esc_url($signupUrl); ?>">
728 812 <?php esc_html_e('Signup', 'fluent-community'); ?>
@@ -759,9 +843,9 @@
759 843 $frameData['defaults'] = [
760 844 'email' => $invitation ? $invitation->message : ''
761 845 ];
762 846
763 - if (AuthHelper::isRegistrationEnabled()) {
847 + if ($invitation || AuthHelper::isRegistrationEnabled()) {
764 848 $frameData['signupUrl'] = $signupUrl;
765 849 }
766 850
767 851 $frameData['settings'] = $formSettings;
@@ -776,8 +860,14 @@
776 860 public function renderRegistrationForm($frameData, $invitation = null)
777 861 {
778 862 $formFields = AuthHelper::getFormFields($invitation);
779 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 +
780 870 $authSettings = AuthenticationService::getAuthSettings();
781 871
782 872 $termsField = Arr::get($authSettings, 'signup.form.fields.terms');
783 873
@@ -826,14 +916,12 @@
826 916 }
827 917
828 918 add_action('fluent_community/before_registration_form', function ($frameData) {
829 919 if (AuthHelper::isFluentAuthAvailable()) {
830 - $currentUrl = home_url(add_query_arg($_GET, $GLOBALS['wp']->request)); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
920 + $currentUrl = esc_url(home_url(add_query_arg($_GET, $GLOBALS['wp']->request))); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
831 921
832 - ob_start();
833 922 $titlePrefix = __('Signup with', 'fluent-community');
834 - do_shortcode('[fs_auth_buttons redirect="' . $currentUrl . '" title_prefix="' . $titlePrefix . ' " title=""]');
835 - $html = ob_get_clean();
923 + $html = do_shortcode('[fs_auth_buttons redirect="' . $currentUrl . '" title_prefix="' . $titlePrefix . ' " title=""]');
836 924
837 925 if ($html) {
838 926 echo '<div class="fcom_social_auth_wrap">';
839 927 echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped