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 +442 -105 1.0.972.11.0 View file →
@@ -4,14 +4,19 @@
4 4 namespace FluentCommunity\Modules\Auth;
5 5
6 6 use FluentAuth\App\Hooks\Handlers\CustomAuthHandler;
7 7 use FluentCommunity\App\App;
8 +use FluentCommunity\App\Functions\Utility;
9 +use FluentCommunity\App\Services\AuthenticationService;
8 10 use FluentCommunity\App\Models\BaseSpace;
9 11 use FluentCommunity\App\Models\User;
12 +use FluentCommunity\App\Services\FeedsHelper;
10 13 use FluentCommunity\App\Services\Helper;
11 14 use FluentCommunity\App\Services\ProfileHelper;
12 15 use FluentCommunity\App\Vite;
13 16 use FluentCommunity\Framework\Support\Arr;
17 +use FluentCommunity\Modules\Auth\Classes\Invitation;
18 +use FluentCommunity\Modules\Auth\Classes\InvitationHandler;
14 19 use FluentCommunity\Modules\Auth\Classes\InvitationService;
15 20
16 21 class AuthModdule
17 22 {
@@ -22,72 +27,195 @@
22 27 add_action('wp_ajax_nopriv_fcom_user_registration', [$this, 'handleUserSignup']);
23 28 add_action('wp_ajax_fcom_user_registration', [$this, 'handleUserSignup']);
24 29 add_action('wp_ajax_nopriv_fcom_user_login_form', [$this, 'handleUserLogin']);
25 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);
26 54 }
27 55
28 56 public function maybeAutoLogin($requestData)
29 57 {
30 58 $urlHash = Arr::get($requestData, 'fcom_url_hash');
31 - if ($urlHash) {
59 + if ($urlHash && !get_current_user_id()) {
32 60 $tagetUser = ProfileHelper::getUserByUrlHash($urlHash);
33 -
34 61 if ($tagetUser) {
35 62 $willAtoLogin = apply_filters('fluent_community/allow_auto_login_by_url', !user_can($tagetUser, 'delete_pages'), $tagetUser);
36 - // $willAtoLogin = true;
37 63 if ($willAtoLogin) {
38 - InvitationService::makeLogin($tagetUser);
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 + }
39 71 }
40 72 }
41 73 }
42 74
43 75 // Remove fcom_action and fcom_url_hash from the current url
44 - $currentUrl = home_url(add_query_arg($_GET, $GLOBALS['wp']->request));
76 + $currentUrl = home_url(add_query_arg($_GET, $GLOBALS['wp']->request)); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
45 77 $url = remove_query_arg(['fcom_action', 'fcom_url_hash'], $currentUrl);
46 - wp_redirect($url);
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
47 100 exit();
48 101 }
49 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 +
50 122 public function viewAuthPage()
51 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 +
52 132 $currentUserId = get_current_user_id();
53 133 // check if there has any invitation token
54 - $inivtationToken = Arr::get($_GET, 'invitation_token');
134 + $inivtationToken = Arr::get($_GET, 'invitation_token'); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
55 135
56 136 $inviation = null;
57 137 if ($inivtationToken) {
58 138 $inviation = apply_filters('fluent_community/auth/invitation', null, $inivtationToken);
139 + if ($inviation && !$inviation->isValid()) {
140 + $inviation = null;
141 + }
59 142 }
60 143
61 144 if ($currentUserId && !$inviation) {
62 - wp_redirect(Helper::baseUrl());
63 - exit();
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);
64 154 }
65 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 +
66 176 do_action('fluent_community/auth/before_auth_page_process', $currentUserId, $inviation);
67 177
68 178 $acceptedForms = ['login', 'register', 'reset_password'];
69 - $targetForm = Arr::get($_GET, 'form');
70 - if (!in_array($targetForm, $acceptedForms)) {
179 + $targetForm = Arr::get($_GET, 'form'); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
180 + $explicitForm = in_array($targetForm, $acceptedForms, true);
181 + if (!$explicitForm) {
71 182 $targetForm = 'login';
72 183 }
73 184
74 - if ($inviation && $targetForm != 'reset_password') {
75 - $isUserAvailable = get_user_by('email', $inviation->message);
76 - $targetForm = $isUserAvailable ? 'login' : 'register';
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 + }
77 192 }
78 193
79 - if ($inviation && $currentUserId) {
80 - $invitedUser = get_user_by('email', $inviation->message);
81 - if ($invitedUser && $invitedUser->ID == $currentUserId) {
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 {
82 201 $targetForm = 'accept_invitation';
83 202 }
84 203 }
85 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 +
86 215 $isFluentAuth = AuthHelper::isFluentAuthAvailable();
87 216 if (!$isFluentAuth && $targetForm == 'reset_password') {
88 - wp_redirect(wp_lostpassword_url(Helper::baseUrl()));
89 - exit();
217 + $this->safeRedirectAndExit(wp_lostpassword_url(Helper::baseUrl()));
90 218 }
91 219
92 220 $portalSettings = Helper::generalSettings();
93 221 $titleVar = Arr::get($portalSettings, 'site_title');
@@ -92,60 +220,86 @@
92 220 $portalSettings = Helper::generalSettings();
93 221 $titleVar = Arr::get($portalSettings, 'site_title');
94 222
95 223 $frameData = [
96 - 'logo' => Arr::get($portalSettings, 'logo', ''),
97 - 'title' => sprintf(__('Join %s', 'fluent-community'), $titleVar),
98 - 'description' => __('Login or Signup to join the community', 'fluent-community'),
99 - 'loginBtnText' => __('Login', 'fluent-community'),
100 - 'signupBtnText' => __('Signup', 'fluent-community'),
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'),
101 229 ];
102 230
103 - $currentUrl = home_url(add_query_arg($_GET, $GLOBALS['wp']->request));
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 + }
104 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 +
105 260 $pageVars = [
106 261 'title' => $frameData['title'],
262 + 'og_title' => $frameData['title'],
107 263 'description' => $frameData['description'],
108 264 'url' => $currentUrl,
109 265 'featured_image' => '',
110 - 'css_files' => [
111 - Vite::getDynamicSrcUrl('theme-default.scss'),
112 - Vite::getDynamicSrcUrl('public/scss/user_registration.scss')
113 - ],
114 - 'js_files' => [
115 - Vite::getDynamicSrcUrl('public/js/user_registration.js')
116 - ],
117 - 'js_vars' => [
118 - 'fluentComRegistration' => [
119 - 'ajax_url' => admin_url('admin-ajax.php'),
120 - 'is_logged_in' => is_user_logged_in(),
121 - ]
122 - ],
266 + 'css_files' => [],
267 + 'js_files' => [],
268 + 'js_vars' => [],
123 269 'scope' => 'user_registration',
124 270 'layout' => 'signup',
125 271 'portal' => [
126 272 'logo' => Arr::get($portalSettings, 'logo', ''),
127 - 'title' => \sprintf( __('Welcome to %s', 'fluent-community'), Arr::get($portalSettings, 'site_title') ),
273 + /* translators: %s is replaced by the title of the site */
274 + 'title' => \sprintf(__('Welcome to %s', 'fluent-community'), Arr::get($portalSettings, 'site_title')),
128 275 'description' => get_bloginfo('description')
129 276 ]
130 277 ];
131 278
132 - if ($isFluentAuth) {
133 - $pageVars['js_files'][] = FLUENT_AUTH_PLUGIN_URL . 'dist/public/login_helper.js';
134 - $pageVars['js_vars']['fluentAuthPublic'] = [
135 - 'hide' => false,
136 - 'redirect_fallback' => site_url(),
137 - 'fls_login_nonce' => wp_create_nonce('fsecurity_login_nonce'),
138 - 'ajax_url' => admin_url('admin-ajax.php'),
139 - 'i18n' => [
140 - 'Username_or_Email' => __('Email Address', 'fluent-community'),
141 - 'Password' => __('Password', 'fluent-community')
142 - ]
279 + if (Utility::isDev()) {
280 + $pageVars['js_files'] = [
281 + Vite::getStaticSrcUrl('public/js/user_registration.js')
143 282 ];
144 283 }
145 284
146 - add_action('fluent_community/headless/content', function ($context) use ($targetForm, $currentUrl, $frameData, $inviation) {
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 +
147 300 if ($targetForm == 'login') {
301 + $frameData['button_label'] = Arr::get($formSettings, 'form.button_label', __('Login', 'fluent-community'));
148 302 $this->showLoginForm($frameData, $inviation);
149 303 } else if ($targetForm == 'reset_password') {
150 304 $frameData['title'] = __('Reset your password', 'fluent-community');
151 305 ?>
@@ -153,8 +307,11 @@
153 307 <div class="fcom_onboard_header">
154 308 <div class="fcom_onboard_header_title">
155 309 <h2><?php echo esc_html($frameData['title']); ?></h2>
156 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>
157 314 </div>
158 315 <div class="fcom_onboard_body">
159 316 <div class="fcom_onboard_form">
160 317 <?php echo do_shortcode('[fluent_auth_reset_password redirect_to="' . esc_url($currentUrl) . '"]'); ?>
@@ -160,9 +317,9 @@
160 317 <?php echo do_shortcode('[fluent_auth_reset_password redirect_to="' . esc_url($currentUrl) . '"]'); ?>
161 318 <div class="fcom_spaced_divider">
162 319 <div class="fcom_alt_auth_text">
163 320 <a href="<?php echo esc_url(add_query_arg('form', 'login', $currentUrl)); ?>">
164 - <?php _e('Back to Login', 'fluent-community'); ?>
321 + <?php esc_html_e('Back to Login', 'fluent-community'); ?>
165 322 </a>
166 323 </div>
167 324 </div>
168 325 </div>
@@ -169,13 +326,13 @@
169 326 </div>
170 327 </div>
171 328 <?php
172 329 } else if ($targetForm == 'accept_invitation') {
173 - do_action('fluent_community/auth/show_inviration_for_user', $inviation, $frameData);
330 + do_action('fluent_community/auth/show_invitation_for_user', $inviation, $frameData);
174 331 } else {
175 - //check if the registration is disabled
176 - if (!AuthHelper::isRegistrationEnabled()) {
177 - echo '<div class="fcom_completed"><div class="fcom_complted_header"><h4>' . __('Registration is disabled for this community', 'fluent-community') . '</h4>';
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>';
178 335 return;
179 336 }
180 337
181 338 $frameData['hiddenFields'] = [
@@ -180,20 +337,45 @@
180 337
181 338 $frameData['hiddenFields'] = [
182 339 'register' => 'yes',
183 340 'action' => 'fcom_user_signup',
184 - '_fls_signup_nonce' => wp_create_nonce('fluent_auth_signup_nonce')
341 + '_fcom_signup_nonce' => wp_create_nonce('fluent_auth_signup_nonce')
185 342 ];
186 343
187 344 $frameData['loginUrl'] = add_query_arg('form', 'login', $currentUrl);
188 - $frameData['description'] = __('Create an account to join the community', 'fluent-community');
345 + $frameData = wp_parse_args(Arr::get($formSettings, 'form'), $frameData);
346 +
189 347 $this->renderRegistrationForm($frameData, $inviation);
190 348 }
191 349 }, 10, 1);
192 350
193 - status_header(200);
194 - App::make('view')->render('headless_page', $pageVars);
195 - exit(200);
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);
196 378 }
197 379
198 380 public function handleUserSignup()
199 381 {
@@ -200,11 +382,26 @@
200 382 if (is_user_logged_in()) {
201 383 return $this->handleSignupCompleted(get_current_user_id());
202 384 }
203 385
204 - if (!AuthHelper::isRegistrationEnabled()) {
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')) {
205 388 wp_send_json([
206 - 'message' => __('Registration is disabled for this community', 'fluent-community')
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')
207 404 ], 422);
208 405 }
209 406
210 407 $app = App::make('app');
@@ -210,10 +407,15 @@
210 407 $app = App::make('app');
211 408 $request = $app->make('request');
212 409 $fields = AuthHelper::getFormFields();
213 410
411 + $authSettings = AuthenticationService::getAuthSettings();
412 + $termsField = Arr::get($authSettings, 'signup.form.fields.terms');
413 +
414 + $fields['terms'] = $termsField ?: $fields['terms'];
415 +
214 416 $requiredFields = array_filter($fields, function ($field) {
215 - return $field['required'] ?? false;
417 + return ($field['required'] && empty($field['disabled'])) ?? false;
216 418 });
217 419
218 420 $keys = array_keys($fields);
219 421 $data = Arr::only($request->all(), $keys);
@@ -222,9 +424,9 @@
222 424 $data['username'] = sanitize_user(strtolower(preg_replace('/[^A-Za-z0-9_]/', '', $data['username'])));
223 425
224 426 if (empty($data['username'])) {
225 427 wp_send_json([
226 - 'message' => __('Username is not valid', 'fluent-community'),
428 + 'message' => esc_html__('Username is not valid', 'fluent-community'),
227 429 'errors' => [
228 430 'username' => __('Please provide a valid username', 'fluent-community')
229 431 ]
230 432 ], 422);
@@ -231,9 +433,9 @@
231 433 }
232 434
233 435 if (!ProfileHelper::isUsernameAvailable($data['username'])) {
234 436 wp_send_json([
235 - 'message' => __('Username is already taken', 'fluent-community'),
437 + 'message' => esc_html__('Username is already taken', 'fluent-community'),
236 438 'errors' => [
237 439 'username' => __('Username is already taken. Please use a different username', 'fluent-community')
238 440 ]
239 441 ], 422);
@@ -238,9 +440,39 @@
238 440 ]
239 441 ], 422);
240 442 }
241 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 +
242 473 $data['email'] = sanitize_email($data['email']);
474 + $data['full_name'] = sanitize_text_field(Arr::get($data, 'full_name', ''));
243 475
244 476 $validations = [
245 477 'full_name' => 'required|max:100|string',
246 478 'username' => 'required|unique:users,user_login|unique:fcom_xprofile,username|min:4|max:30',
@@ -297,16 +529,8 @@
297 529 unset($data['full_name']);
298 530 $data = array_filter($data);
299 531 }
300 532
301 - if (AuthHelper::isFluentAuthAvailable()) {
302 - $data = wp_parse_args($data, $request->all());
303 - $this->handleSignupViaFlentAuth($data);
304 - wp_send_json([
305 - 'message' => __('Something is not working! Please try again', 'fluent-community')
306 - ], 422);
307 - }
308 -
309 533 $rateLimit = AuthHelper::isAuthRateLimit();
310 534
311 535 if (is_wp_error($rateLimit)) {
312 536 wp_send_json([
@@ -358,16 +582,14 @@
358 582 }
359 583
360 584 private function handleSignupViaFlentAuth($data)
361 585 {
362 - add_filter('fluent_auth/signup_form_data', function ($requestData) use ($data) {
363 - return $data;
364 - });
365 -
366 586 add_action('fluent_auth/after_creating_user', function ($userId) {
367 587 $this->handleSignupCompleted($userId);
368 588 }, 1, 1);
369 589
590 + add_filter('fluent_auth/signup_enabled', '__return_true');
591 +
370 592 (new CustomAuthHandler())->handleSignupAjax();
371 593 }
372 594
373 595 private function handleSignupCompleted($userId)
@@ -377,14 +599,18 @@
377 599 $user->syncXProfile(true, true);
378 600
379 601 $redirectUrl = Helper::baseUrl();
380 602
381 - $redirectUrl = apply_filters('fluent_community/auth/after_signup_redirect_url', $redirectUrl, $user, $_REQUEST);
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
382 608 $btnText = __('Continue to the community', 'fluent-community');
383 609
384 610 $html = '<div class="fcom_completed"><div class="fcom_complted_header"><h2>' . __('Congratulations!', 'fluent-community') . '</h2>';
385 611 $html .= '<p>' . __('You have successfully registered to the community', 'fluent-community') . '</p></div>';
386 - $html .= '<a href="' . $redirectUrl . '" class="fcom_btn fcom_btn_success">' . $btnText . '</a>';
612 + $html .= '<a href="' . esc_url($redirectUrl) . '" class="fcom_btn fcom_btn_success">' . $btnText . '</a>';
387 613 $html .= '</div>';
388 614
389 615 if (!get_current_user_id()) {
390 616 $wpUser = get_user_by('ID', $userId);
@@ -409,8 +635,15 @@
409 635 'message' => __('This form cannot be used to log in. Please reload the page and try again.', 'fluent-community')
410 636 ], 422);
411 637 }
412 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 +
413 646 $app = App::make('app');
414 647 $request = $app->make('request');
415 648
416 649 $data = $request->all();
@@ -439,10 +672,16 @@
439 672
440 673 $user = wp_authenticate($data['log'], $data['pwd']);
441 674
442 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 + }
443 682 wp_send_json([
444 - 'message' => $user->get_error_message()
683 + 'message' => $message
445 684 ], 422);
446 685 }
447 686
448 687 InvitationService::makeLogin($user);
@@ -447,10 +686,10 @@
447 686
448 687 InvitationService::makeLogin($user);
449 688
450 689 $redirectUrl = null;
451 - if (!empty($_REQUEST['redirect_to'])) {
452 - $redirectUrl = sanitize_url($_REQUEST['redirect_to']);
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
453 692 }
454 693
455 694 if (!$redirectUrl) {
456 695 $redirectUrl = Helper::baseUrl();
@@ -476,9 +715,9 @@
476 715 $btnText = __('Continue to the community', 'fluent-community');
477 716
478 717 $html = '<div class="fcom_completed"><div class="fcom_complted_header"><h2>' . __('Welcome back!', 'fluent-community') . '</h2>';
479 718 $html .= '<p>' . __('You have successfully logged in to the community', 'fluent-community') . '</p></div>';
480 - $html .= '<a href="' . $redirectUrl . '" class="fcom_btn fcom_btn_success">' . $btnText . '</a>';
719 + $html .= '<a href="' . esc_url($redirectUrl) . '" class="fcom_btn fcom_btn_success">' . $btnText . '</a>';
481 720 $html .= '</div>';
482 721
483 722 wp_send_json([
484 723 'success_html' => $html,
@@ -489,9 +728,12 @@
489 728 public function showLoginForm($frameData, $invitation = null)
490 729 {
491 730 $portalSettings = Helper::generalSettings();
492 731 $isFluentAuth = AuthHelper::isFluentAuthAvailable();
493 - $currentUrl = home_url(add_query_arg($_GET, $GLOBALS['wp']->request));
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 */
494 736 $title = sprintf(__('Login to %s', 'fluent-community'), Arr::get($portalSettings, 'site_title'));
495 737
496 738 $description = '';
497 739 if ($invitation) {
@@ -501,21 +743,62 @@
501 743 if ($space) {
502 744 $title = $space->title . ' - ' . Arr::get($portalSettings, 'site_title');
503 745 }
504 746 }
505 - $description = sprintf(__('%s has invited you to join this community. Please login to accept your invitation.', 'fluent-community'), $invitationBy);
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 + });
506 756 }
507 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 +
508 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 + });
509 787 ?>
510 788 <div id="fcom_user_onboard_wrap" class="fcom_user_onboard">
511 789 <div class="fcom_onboard_header">
790 + <?php do_action('fluent_community/before_auth_form_header', 'login'); ?>
512 791 <div class="fcom_onboard_header_title">
513 - <h2><?php echo esc_html($title); ?></h2>
792 + <?php if (!empty($formSettings['title'])): ?>
793 + <h2>
794 + <?php echo esc_html($formSettings['title']); ?>
795 + </h2>
796 + <?php endif; ?>
514 797 </div>
515 - <?php if ($description): ?>
798 + <?php if (!empty($formSettings['description'])): ?>
516 799 <div class="fcom_onboard_sub">
517 - <p><?php echo wp_kses_post($description); ?></p>
800 + <?php echo wp_kses_post(trim($formSettings['description'])); ?>
518 801 </div>
519 802 <?php endif; ?>
520 803 </div>
521 804 <div class="fcom_onboard_body">
@@ -521,19 +804,19 @@
521 804 <div class="fcom_onboard_body">
522 805 <div class="fcom_onboard_form">
523 806 <?php echo do_shortcode('[fluent_auth_login redirect_to="' . esc_url($currentUrl) . '"]'); ?>
524 807 <div class="fcom_spaced_divider">
525 - <?php if (AuthHelper::isRegistrationEnabled()): ?>
808 + <?php if ($invitation || AuthHelper::isRegistrationEnabled()): ?>
526 809 <div class="fcom_alt_auth_text">
527 - <?php _e('Don\'t have an account?', 'fluent-community'); ?>
528 - <a href="<?php echo esc_url(add_query_arg('form', 'register', $currentUrl)); ?>">
529 - <?php _e('Signup', 'fluent-community'); ?>
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'); ?>
530 813 </a>
531 814 </div>
532 815 <?php endif; ?>
533 816 <p class="fcom_reset_pass_text">
534 - <a href="<?php echo esc_url(add_query_arg('form', 'reset_password', $currentUrl)); ?>">
535 - <?php _e('Lost your password?', 'fluent-community'); ?>
817 + <a href="<?php echo esc_url(AuthHelper::getLostPasswordUrl($currentUrl)); ?>">
818 + <?php esc_html_e('Lost your password?', 'fluent-community'); ?>
536 819 </a>
537 820 </p>
538 821 </div>
539 822 </div>
@@ -544,11 +827,14 @@
544 827 }
545 828
546 829 $frameData['redirect'] = $currentUrl;
547 830
548 - $frameData['hiddenFields'] = [];
831 + $frameData['hiddenFields'] = [
832 + 'action' => 'fcom_user_login_form',
833 + '_fcom_login_nonce' => wp_create_nonce('fcom_user_login_nonce'),
834 + ];
549 835 if ($invitation) {
550 - $frameData['loginBtnText'] = __('Log In & Accept Invitation', 'fluent-community');
836 + $frameData['button_label'] = __('Log In & Accept Invitation', 'fluent-community');
551 837 $frameData['hiddenFields']['invitation_token'] = $invitation->message_rendered;
552 838 }
553 839
554 840 $frameData['title'] = $title;
@@ -557,14 +843,16 @@
557 843 $frameData['defaults'] = [
558 844 'email' => $invitation ? $invitation->message : ''
559 845 ];
560 846
561 - if (AuthHelper::isRegistrationEnabled()) {
562 - $frameData['signupUrl'] = add_query_arg('form', 'register', $currentUrl);
847 + if ($invitation || AuthHelper::isRegistrationEnabled()) {
848 + $frameData['signupUrl'] = $signupUrl;
563 849 }
564 850
565 - if (isset($_GET['redirect_to'])) {
566 - $frameData['redirect_to'] = sanitize_url($_GET['redirect_to']);
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
567 855 }
568 856
569 857 App::make('view')->render('auth.login_form', $frameData);
570 858 }
@@ -572,8 +860,28 @@
572 860 public function renderRegistrationForm($frameData, $invitation = null)
573 861 {
574 862 $formFields = AuthHelper::getFormFields($invitation);
575 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 +
576 884 $frameData['formFields'] = $formFields;
577 885
578 886 if ($invitation) {
579 887 $frameData['hiddenFields'] = [
@@ -578,21 +886,50 @@
578 886 if ($invitation) {
579 887 $frameData['hiddenFields'] = [
580 888 'invitation_token' => $invitation->message_rendered,
581 889 'action' => 'fcom_user_registration',
582 - '_fls_signup_nonce' => wp_create_nonce('fluent_auth_signup_nonce')
890 + '_fcom_signup_nonce' => wp_create_nonce('fluent_auth_signup_nonce')
583 891 ];
584 892
585 893 $invitationBy = $invitation->xprofile ? $invitation->xprofile->display_name : __('Someone', 'fluent-community');
586 - $frameData['description'] = sprintf(__('%s has invited you to join this community. Please create an account to accept your invitation.', 'fluent-community'), $invitationBy);
587 - $frameData['signupBtnText'] = __('Register & Accept invitation', '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');
588 906 } else {
589 907 $frameData['hiddenFields'] = [
590 908 'register' => 'yes',
591 909 'action' => 'fcom_user_registration',
592 - '_fls_signup_nonce' => wp_create_nonce('fluent_auth_signup_nonce')
910 + '_fcom_signup_nonce' => wp_create_nonce('fluent_auth_signup_nonce'),
593 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 + }
594 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 + });
595 932
596 933 App::make('view')->render('auth.user_invitation', $frameData);
597 934 }
598 935 }