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 +112 -35 2.5.02.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 }
@@ -63,12 +74,52 @@
63 74
64 75 // Remove fcom_action and fcom_url_hash from the current url
65 76 $currentUrl = home_url(add_query_arg($_GET, $GLOBALS['wp']->request)); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
66 77 $url = remove_query_arg(['fcom_action', 'fcom_url_hash'], $currentUrl);
67 - 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
68 100 exit();
69 101 }
70 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 +
71 122 public function viewAuthPage()
72 123 {
73 124
74 125 add_filter('login_form_defaults', function ($defaults) {
@@ -98,20 +149,19 @@
98 149 if (!$redirectUrl) {
99 150 $redirectUrl = Helper::baseUrl();
100 151 }
101 152
102 - wp_safe_redirect($redirectUrl);
103 - exit();
153 + $this->safeRedirectAndExit($redirectUrl);
104 154 }
105 155
106 156 if ($currentUserId && $inviation) {
157 + /** @var BaseSpace|null $space */
107 158 $space = BaseSpace::withoutGlobalScopes()->find($inviation->post_id);
108 159 if ($space) {
109 160 if (Helper::isUserInSpace($currentUserId, $inviation->post_id)) {
110 161 // let's redirect the user to the space
111 162 $redirectUrl = $space->getPermalink();
112 - wp_safe_redirect($redirectUrl);
113 - exit();
163 + $this->safeRedirectAndExit($redirectUrl);
114 164 }
115 165
116 166 if (!empty($_REQUEST['auto_accept'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
117 167 $redirectUrl = (new InvitationHandler())->handleInvitationLogin(Helper::baseUrl(), get_user_by('ID', $currentUserId), $inviation->message_rendered);
@@ -117,10 +167,9 @@
117 167 $redirectUrl = (new InvitationHandler())->handleInvitationLogin(Helper::baseUrl(), get_user_by('ID', $currentUserId), $inviation->message_rendered);
118 168 if (is_wp_error($redirectUrl) || !$redirectUrl) {
119 169 $redirectUrl = Helper::baseUrl();
120 170 }
121 - wp_safe_redirect($redirectUrl);
122 - exit();
171 + $this->safeRedirectAndExit($redirectUrl);
123 172 }
124 173 }
125 174 }
126 175
@@ -152,12 +201,21 @@
152 201 $targetForm = 'accept_invitation';
153 202 }
154 203 }
155 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 +
156 215 $isFluentAuth = AuthHelper::isFluentAuthAvailable();
157 216 if (!$isFluentAuth && $targetForm == 'reset_password') {
158 - wp_safe_redirect(wp_lostpassword_url(Helper::baseUrl()));
159 - exit();
217 + $this->safeRedirectAndExit(wp_lostpassword_url(Helper::baseUrl()));
160 218 }
161 219
162 220 $portalSettings = Helper::generalSettings();
163 221 $titleVar = Arr::get($portalSettings, 'site_title');
@@ -174,10 +232,9 @@
174 232 $frameData['button_label'] = __('Signup', 'fluent-community');
175 233 if (!$inviation) {
176 234 $customSignupUrl = Arr::get($portalSettings, 'custom_signup_url');
177 235 if ($customSignupUrl) {
178 - wp_safe_redirect($customSignupUrl);
179 - exit();
236 + $this->safeRedirectAndExit($customSignupUrl);
180 237 }
181 238 }
182 239 }
183 240
@@ -190,9 +247,13 @@
190 247 wp_enqueue_script('fluent_auth_scripts', Vite::getStaticSrcUrl('user_registration.js'), [], FLUENT_COMMUNITY_PLUGIN_VERSION, true);
191 248 wp_localize_script('fluent_auth_scripts', 'fluentComRegistration', array(
192 249 'ajax_url' => admin_url('admin-ajax.php'),
193 250 'is_logged_in' => is_user_logged_in(),
194 - '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 + ]
195 256 ));
196 257 }
197 258 }, 10);
198 259
@@ -267,10 +328,10 @@
267 328 <?php
268 329 } else if ($targetForm == 'accept_invitation') {
269 330 do_action('fluent_community/auth/show_invitation_for_user', $inviation, $frameData);
270 331 } else {
271 - //check if the registration is disabled
272 - if (!AuthHelper::isRegistrationEnabled()) {
332 + //check if the registration is disabled (a valid invitation still allows signup)
333 + if (!$inviation && !AuthHelper::isRegistrationEnabled()) {
273 334 echo '<div class="fcom_completed"><div class="fcom_complted_header"><h4>' . esc_html__('Registration is disabled for this community', 'fluent-community') . '</h4>';
274 335 return;
275 336 }
276 337
@@ -288,15 +349,20 @@
288 349 }, 10, 1);
289 350
290 351 add_action('fluent_community/headless/head_early', function ($scope) use ($formSettings) {
291 352 $bannerColors = array_filter(Arr::only($formSettings['banner'], ['title_color', 'text_color', 'background_color']));
292 - $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" />
293 362 <link rel="canonical" href="<?php echo esc_url(Helper::getAuthUrl()); ?>" />
294 363 <style>
295 - .fcom_layout_side {
296 - <?php foreach ($bannerColors as $colorKey => $colorValue): ?> --fcom_ <?php echo esc_html($colorKey); ?>: <?php echo esc_html($colorValue); ?>;
297 - <?php endforeach; ?>
298 - }
364 + .fcom_layout_side { <?php echo esc_html($sideVars); ?> }
299 365 <?php echo esc_html($css); ?>
300 366 </style>
301 367 <?php
302 368 });
@@ -307,11 +373,9 @@
307 373 add_filter('pre_get_document_title', function ($title) use ($frameData) {
308 374 return $frameData['title'];
309 375 }, 9999, 1);
310 376
311 - status_header(200);
312 - App::make('view')->render('headless_page', $pageVars);
313 - exit(200);
377 + $this->renderPageAndExit('headless_page', $pageVars);
314 378 }
315 379
316 380 public function handleUserSignup()
317 381 {
@@ -318,14 +382,8 @@
318 382 if (is_user_logged_in()) {
319 383 return $this->handleSignupCompleted(get_current_user_id());
320 384 }
321 385
322 - if (!AuthHelper::isRegistrationEnabled()) {
323 - wp_send_json([
324 - 'message' => esc_html__('Registration is disabled for this community', 'fluent-community')
325 - ], 422);
326 - }
327 -
328 386 $signupNonce = isset($_POST['_fcom_signup_nonce']) ? sanitize_text_field(wp_unslash($_POST['_fcom_signup_nonce'])) : '';
329 387 if (!$signupNonce || !wp_verify_nonce($signupNonce, 'fluent_auth_signup_nonce')) {
330 388 wp_send_json([
331 389 'message' => esc_html__('Invalid request. Please refresh the page and try again.', 'fluent-community')
@@ -331,8 +389,22 @@
331 389 'message' => esc_html__('Invalid request. Please refresh the page and try again.', 'fluent-community')
332 390 ], 403);
333 391 }
334 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 +
335 407 $app = App::make('app');
336 408 $request = $app->make('request');
337 409 $fields = AuthHelper::getFormFields();
338 410
@@ -398,8 +470,9 @@
398 470 ], 422);
399 471 }
400 472
401 473 $data['email'] = sanitize_email($data['email']);
474 + $data['full_name'] = sanitize_text_field(Arr::get($data, 'full_name', ''));
402 475
403 476 $validations = [
404 477 'full_name' => 'required|max:100|string',
405 478 'username' => 'required|unique:users,user_login|unique:fcom_xprofile,username|min:4|max:30',
@@ -527,9 +600,9 @@
527 600
528 601 $redirectUrl = Helper::baseUrl();
529 602
530 603 if (!empty($_REQUEST['redirect_to'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
531 - $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
532 605 }
533 606
534 607 $redirectUrl = apply_filters('fluent_community/auth/after_signup_redirect_url', $redirectUrl, $user, $_REQUEST); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
535 608 $btnText = __('Continue to the community', 'fluent-community');
@@ -731,9 +804,9 @@
731 804 <div class="fcom_onboard_body">
732 805 <div class="fcom_onboard_form">
733 806 <?php echo do_shortcode('[fluent_auth_login redirect_to="' . esc_url($currentUrl) . '"]'); ?>
734 807 <div class="fcom_spaced_divider">
735 - <?php if (AuthHelper::isRegistrationEnabled()): ?>
808 + <?php if ($invitation || AuthHelper::isRegistrationEnabled()): ?>
736 809 <div class="fcom_alt_auth_text">
737 810 <?php esc_html_e('Don\'t have an account?', 'fluent-community'); ?>
738 811 <a href="<?php echo esc_url($signupUrl); ?>">
739 812 <?php esc_html_e('Signup', 'fluent-community'); ?>
@@ -770,9 +843,9 @@
770 843 $frameData['defaults'] = [
771 844 'email' => $invitation ? $invitation->message : ''
772 845 ];
773 846
774 - if (AuthHelper::isRegistrationEnabled()) {
847 + if ($invitation || AuthHelper::isRegistrationEnabled()) {
775 848 $frameData['signupUrl'] = $signupUrl;
776 849 }
777 850
778 851 $frameData['settings'] = $formSettings;
@@ -787,8 +860,14 @@
787 860 public function renderRegistrationForm($frameData, $invitation = null)
788 861 {
789 862 $formFields = AuthHelper::getFormFields($invitation);
790 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 +
791 870 $authSettings = AuthenticationService::getAuthSettings();
792 871
793 872 $termsField = Arr::get($authSettings, 'signup.form.fields.terms');
794 873
@@ -837,14 +916,12 @@
837 916 }
838 917
839 918 add_action('fluent_community/before_registration_form', function ($frameData) {
840 919 if (AuthHelper::isFluentAuthAvailable()) {
841 - $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
842 921
843 - ob_start();
844 922 $titlePrefix = __('Signup with', 'fluent-community');
845 - do_shortcode('[fs_auth_buttons redirect="' . $currentUrl . '" title_prefix="' . $titlePrefix . ' " title=""]');
846 - $html = ob_get_clean();
923 + $html = do_shortcode('[fs_auth_buttons redirect="' . $currentUrl . '" title_prefix="' . $titlePrefix . ' " title=""]');
847 924
848 925 if ($html) {
849 926 echo '<div class="fcom_social_auth_wrap">';
850 927 echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped