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