PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.10.01
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.10.01
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 +384 -123 1.1.02.10.01 View file →
@@ -4,15 +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;
8 9 use FluentCommunity\App\Services\AuthenticationService;
9 10 use FluentCommunity\App\Models\BaseSpace;
10 11 use FluentCommunity\App\Models\User;
12 +use FluentCommunity\App\Services\FeedsHelper;
11 13 use FluentCommunity\App\Services\Helper;
12 14 use FluentCommunity\App\Services\ProfileHelper;
13 15 use FluentCommunity\App\Vite;
14 16 use FluentCommunity\Framework\Support\Arr;
17 +use FluentCommunity\Modules\Auth\Classes\Invitation;
18 +use FluentCommunity\Modules\Auth\Classes\InvitationHandler;
15 19 use FluentCommunity\Modules\Auth\Classes\InvitationService;
16 20
17 21 class AuthModdule
18 22 {
@@ -25,83 +29,165 @@
25 29 add_action('wp_ajax_nopriv_fcom_user_login_form', [$this, 'handleUserLogin']);
26 30 add_action('wp_ajax_fcom_user_login_form', [$this, 'handleUserLogin']);
27 31
28 32 add_filter('fluent_auth/login_redirect_url', function ($redirectUrl, $user) {
29 - if (empty($_REQUEST['is_fcom_auth']) || empty($_REQUEST['fcom_redirect'])) {
33 + if (empty($_REQUEST['is_fcom_auth']) || empty($_REQUEST['fcom_redirect'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
30 34 return $redirectUrl;
31 35 }
32 36
33 37 // validate the url
34 - $redirectUrl = $_REQUEST['fcom_redirect'];
35 - if (!filter_var($redirectUrl, FILTER_VALIDATE_URL)) {
36 - $redirectUrl = Helper::baseUrl();
37 - }
38 + $redirectUrl = wp_validate_redirect(sanitize_url(wp_unslash($_REQUEST['fcom_redirect'])), Helper::baseUrl()); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
38 39
39 40 $redirectUrl = apply_filters('fluent_community/auth/after_login_redirect_url', $redirectUrl, $user);
40 41 return $redirectUrl;
41 42 }, 10, 2);
42 -
43 - add_filter('login_form_defaults', function ($defaults) {
44 - $defaults['label_username'] = __('Email Address', 'fluent-community');
45 - return $defaults;
46 - });
47 -
48 43 }
49 44
50 45 public function maybeAutoLogin($requestData)
51 46 {
52 47 $urlHash = Arr::get($requestData, 'fcom_url_hash');
53 - if ($urlHash) {
48 + if ($urlHash && !get_current_user_id()) {
54 49 $tagetUser = ProfileHelper::getUserByUrlHash($urlHash);
55 -
56 50 if ($tagetUser) {
57 51 $willAtoLogin = apply_filters('fluent_community/allow_auto_login_by_url', !user_can($tagetUser, 'delete_pages'), $tagetUser);
58 - // $willAtoLogin = true;
59 52 if ($willAtoLogin) {
60 - 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 + }
61 60 }
62 61 }
63 62 }
64 63
65 64 // Remove fcom_action and fcom_url_hash from the current url
66 - $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
67 66 $url = remove_query_arg(['fcom_action', 'fcom_url_hash'], $currentUrl);
68 - 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
69 89 exit();
70 90 }
71 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 +
72 111 public function viewAuthPage()
73 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 +
74 121 $currentUserId = get_current_user_id();
75 122 // check if there has any invitation token
76 - $inivtationToken = Arr::get($_GET, 'invitation_token');
123 + $inivtationToken = Arr::get($_GET, 'invitation_token'); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
77 124
78 125 $inviation = null;
79 126 if ($inivtationToken) {
80 127 $inviation = apply_filters('fluent_community/auth/invitation', null, $inivtationToken);
128 + if ($inviation && !$inviation->isValid()) {
129 + $inviation = null;
130 + }
81 131 }
82 132
83 133 if ($currentUserId && !$inviation) {
84 - wp_redirect(Helper::baseUrl());
85 - 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);
86 143 }
87 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 +
88 165 do_action('fluent_community/auth/before_auth_page_process', $currentUserId, $inviation);
89 166
90 167 $acceptedForms = ['login', 'register', 'reset_password'];
91 - $targetForm = Arr::get($_GET, 'form');
92 - 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) {
93 171 $targetForm = 'login';
94 172 }
95 173
96 - if ($inviation && $targetForm != 'reset_password') {
97 - $isUserAvailable = get_user_by('email', $inviation->message);
98 - $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 + }
99 181 }
100 182
101 - if ($inviation && $currentUserId) {
102 - $invitedUser = get_user_by('email', $inviation->message);
103 - 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 {
104 190 $targetForm = 'accept_invitation';
105 191 }
106 192 }
107 193
@@ -106,10 +192,9 @@
106 192 }
107 193
108 194 $isFluentAuth = AuthHelper::isFluentAuthAvailable();
109 195 if (!$isFluentAuth && $targetForm == 'reset_password') {
110 - wp_redirect(wp_lostpassword_url(Helper::baseUrl()));
111 - exit();
196 + $this->safeRedirectAndExit(wp_lostpassword_url(Helper::baseUrl()));
112 197 }
113 198
114 199 $portalSettings = Helper::generalSettings();
115 200 $titleVar = Arr::get($portalSettings, 'site_title');
@@ -114,59 +199,71 @@
114 199 $portalSettings = Helper::generalSettings();
115 200 $titleVar = Arr::get($portalSettings, 'site_title');
116 201
117 202 $frameData = [
118 - 'logo' => Arr::get($portalSettings, 'logo', ''),
119 - 'title' => sprintf(__('Join %s', 'fluent-community'), $titleVar),
120 - 'description' => __('Login or Signup to join the community', 'fluent-community'),
121 - 'loginBtnText' => __('Login', 'fluent-community'),
122 - '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'),
123 208 ];
124 209
125 - $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 + }
126 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 +
127 239 $pageVars = [
128 240 'title' => $frameData['title'],
241 + 'og_title' => $frameData['title'],
129 242 'description' => $frameData['description'],
130 243 'url' => $currentUrl,
131 244 'featured_image' => '',
132 - 'css_files' => [
133 - Vite::getDynamicSrcUrl('theme-default.scss'),
134 - Vite::getStaticSrcUrl('user_registration.css')
135 - ],
136 - 'js_files' => [
137 - Vite::getStaticSrcUrl('public/js/user_registration.js')
138 - ],
139 - 'js_vars' => [
140 - 'fluentComRegistration' => [
141 - 'ajax_url' => admin_url('admin-ajax.php'),
142 - 'is_logged_in' => is_user_logged_in(),
143 - ]
144 - ],
245 + 'css_files' => [],
246 + 'js_files' => [],
247 + 'js_vars' => [],
145 248 'scope' => 'user_registration',
146 249 'layout' => 'signup',
147 250 'portal' => [
148 251 'logo' => Arr::get($portalSettings, 'logo', ''),
149 - 'title' => \sprintf( __('Welcome to %s', 'fluent-community'), Arr::get($portalSettings, 'site_title') ),
252 + /* translators: %s is replaced by the title of the site */
253 + 'title' => \sprintf(__('Welcome to %s', 'fluent-community'), Arr::get($portalSettings, 'site_title')),
150 254 'description' => get_bloginfo('description')
151 255 ]
152 256 ];
153 257
154 - if ($isFluentAuth) {
155 - $pageVars['js_files'][] = FLUENT_AUTH_PLUGIN_URL . 'dist/public/login_helper.js';
156 - $pageVars['js_vars']['fluentAuthPublic'] = [
157 - 'hide' => false,
158 - 'redirect_fallback' => site_url(),
159 - 'fls_login_nonce' => wp_create_nonce('fsecurity_login_nonce'),
160 - 'ajax_url' => admin_url('admin-ajax.php'),
161 - 'i18n' => [
162 - 'Username_or_Email' => __('Email Address', 'fluent-community'),
163 - 'Password' => __('Password', 'fluent-community')
164 - ]
258 + if (Utility::isDev()) {
259 + $pageVars['js_files'] = [
260 + Vite::getStaticSrcUrl('public/js/user_registration.js')
165 261 ];
166 262 }
167 263
168 - $formType = ($targetForm == 'login') ? 'login' : 'signup';
264 + $formType = ($targetForm == 'register') ? 'signup' : 'login';
265 +
169 266 $formSettings = AuthenticationService::getFormattedAuthSettings($formType);
170 267
171 268 if ($formSettings) {
172 269 $pageVars['portal'] = Arr::get($formSettings, 'banner');
@@ -173,9 +270,15 @@
173 270 $pageVars['portal']['form'] = Arr::get($formSettings, 'form');
174 271 }
175 272
176 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 +
177 279 if ($targetForm == 'login') {
280 + $frameData['button_label'] = Arr::get($formSettings, 'form.button_label', __('Login', 'fluent-community'));
178 281 $this->showLoginForm($frameData, $inviation);
179 282 } else if ($targetForm == 'reset_password') {
180 283 $frameData['title'] = __('Reset your password', 'fluent-community');
181 284 ?>
@@ -183,8 +286,11 @@
183 286 <div class="fcom_onboard_header">
184 287 <div class="fcom_onboard_header_title">
185 288 <h2><?php echo esc_html($frameData['title']); ?></h2>
186 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>
187 293 </div>
188 294 <div class="fcom_onboard_body">
189 295 <div class="fcom_onboard_form">
190 296 <?php echo do_shortcode('[fluent_auth_reset_password redirect_to="' . esc_url($currentUrl) . '"]'); ?>
@@ -190,9 +296,9 @@
190 296 <?php echo do_shortcode('[fluent_auth_reset_password redirect_to="' . esc_url($currentUrl) . '"]'); ?>
191 297 <div class="fcom_spaced_divider">
192 298 <div class="fcom_alt_auth_text">
193 299 <a href="<?php echo esc_url(add_query_arg('form', 'login', $currentUrl)); ?>">
194 - <?php _e('Back to Login', 'fluent-community'); ?>
300 + <?php esc_html_e('Back to Login', 'fluent-community'); ?>
195 301 </a>
196 302 </div>
197 303 </div>
198 304 </div>
@@ -201,11 +307,11 @@
201 307 <?php
202 308 } else if ($targetForm == 'accept_invitation') {
203 309 do_action('fluent_community/auth/show_invitation_for_user', $inviation, $frameData);
204 310 } else {
205 - //check if the registration is disabled
206 - if (!AuthHelper::isRegistrationEnabled()) {
207 - 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>';
208 314 return;
209 315 }
210 316
211 317 $frameData['hiddenFields'] = [
@@ -210,9 +316,9 @@
210 316
211 317 $frameData['hiddenFields'] = [
212 318 'register' => 'yes',
213 319 'action' => 'fcom_user_signup',
214 - '_fls_signup_nonce' => wp_create_nonce('fluent_auth_signup_nonce')
320 + '_fcom_signup_nonce' => wp_create_nonce('fluent_auth_signup_nonce')
215 321 ];
216 322
217 323 $frameData['loginUrl'] = add_query_arg('form', 'login', $currentUrl);
218 324 $frameData = wp_parse_args(Arr::get($formSettings, 'form'), $frameData);
@@ -220,11 +326,35 @@
220 326 $this->renderRegistrationForm($frameData, $inviation);
221 327 }
222 328 }, 10, 1);
223 329
224 - status_header(200);
225 - App::make('view')->render('headless_page', $pageVars);
226 - 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);
227 357 }
228 358
229 359 public function handleUserSignup()
230 360 {
@@ -231,11 +361,26 @@
231 361 if (is_user_logged_in()) {
232 362 return $this->handleSignupCompleted(get_current_user_id());
233 363 }
234 364
235 - 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')) {
236 367 wp_send_json([
237 - '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')
238 383 ], 422);
239 384 }
240 385
241 386 $app = App::make('app');
@@ -241,10 +386,15 @@
241 386 $app = App::make('app');
242 387 $request = $app->make('request');
243 388 $fields = AuthHelper::getFormFields();
244 389
390 + $authSettings = AuthenticationService::getAuthSettings();
391 + $termsField = Arr::get($authSettings, 'signup.form.fields.terms');
392 +
393 + $fields['terms'] = $termsField ?: $fields['terms'];
394 +
245 395 $requiredFields = array_filter($fields, function ($field) {
246 - return $field['required'] ?? false;
396 + return ($field['required'] && empty($field['disabled'])) ?? false;
247 397 });
248 398
249 399 $keys = array_keys($fields);
250 400 $data = Arr::only($request->all(), $keys);
@@ -253,9 +403,9 @@
253 403 $data['username'] = sanitize_user(strtolower(preg_replace('/[^A-Za-z0-9_]/', '', $data['username'])));
254 404
255 405 if (empty($data['username'])) {
256 406 wp_send_json([
257 - 'message' => __('Username is not valid', 'fluent-community'),
407 + 'message' => esc_html__('Username is not valid', 'fluent-community'),
258 408 'errors' => [
259 409 'username' => __('Please provide a valid username', 'fluent-community')
260 410 ]
261 411 ], 422);
@@ -262,9 +412,9 @@
262 412 }
263 413
264 414 if (!ProfileHelper::isUsernameAvailable($data['username'])) {
265 415 wp_send_json([
266 - 'message' => __('Username is already taken', 'fluent-community'),
416 + 'message' => esc_html__('Username is already taken', 'fluent-community'),
267 417 'errors' => [
268 418 'username' => __('Username is already taken. Please use a different username', 'fluent-community')
269 419 ]
270 420 ], 422);
@@ -269,8 +419,37 @@
269 419 ]
270 420 ], 422);
271 421 }
272 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 +
273 452 $data['email'] = sanitize_email($data['email']);
274 453
275 454 $validations = [
276 455 'full_name' => 'required|max:100|string',
@@ -328,16 +507,8 @@
328 507 unset($data['full_name']);
329 508 $data = array_filter($data);
330 509 }
331 510
332 - if (AuthHelper::isFluentAuthAvailable()) {
333 - $data = wp_parse_args($data, $request->all());
334 - $this->handleSignupViaFlentAuth($data);
335 - wp_send_json([
336 - 'message' => __('Something is not working! Please try again', 'fluent-community')
337 - ], 422);
338 - }
339 -
340 511 $rateLimit = AuthHelper::isAuthRateLimit();
341 512
342 513 if (is_wp_error($rateLimit)) {
343 514 wp_send_json([
@@ -389,16 +560,14 @@
389 560 }
390 561
391 562 private function handleSignupViaFlentAuth($data)
392 563 {
393 - add_filter('fluent_auth/signup_form_data', function ($requestData) use ($data) {
394 - return $data;
395 - });
396 -
397 564 add_action('fluent_auth/after_creating_user', function ($userId) {
398 565 $this->handleSignupCompleted($userId);
399 566 }, 1, 1);
400 567
568 + add_filter('fluent_auth/signup_enabled', '__return_true');
569 +
401 570 (new CustomAuthHandler())->handleSignupAjax();
402 571 }
403 572
404 573 private function handleSignupCompleted($userId)
@@ -408,14 +577,18 @@
408 577 $user->syncXProfile(true, true);
409 578
410 579 $redirectUrl = Helper::baseUrl();
411 580
412 - $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
413 586 $btnText = __('Continue to the community', 'fluent-community');
414 587
415 588 $html = '<div class="fcom_completed"><div class="fcom_complted_header"><h2>' . __('Congratulations!', 'fluent-community') . '</h2>';
416 589 $html .= '<p>' . __('You have successfully registered to the community', 'fluent-community') . '</p></div>';
417 - $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>';
418 591 $html .= '</div>';
419 592
420 593 if (!get_current_user_id()) {
421 594 $wpUser = get_user_by('ID', $userId);
@@ -440,8 +613,15 @@
440 613 'message' => __('This form cannot be used to log in. Please reload the page and try again.', 'fluent-community')
441 614 ], 422);
442 615 }
443 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 +
444 624 $app = App::make('app');
445 625 $request = $app->make('request');
446 626
447 627 $data = $request->all();
@@ -470,10 +650,16 @@
470 650
471 651 $user = wp_authenticate($data['log'], $data['pwd']);
472 652
473 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 + }
474 660 wp_send_json([
475 - 'message' => $user->get_error_message()
661 + 'message' => $message
476 662 ], 422);
477 663 }
478 664
479 665 InvitationService::makeLogin($user);
@@ -478,10 +664,10 @@
478 664
479 665 InvitationService::makeLogin($user);
480 666
481 667 $redirectUrl = null;
482 - if (!empty($_REQUEST['redirect_to'])) {
483 - $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
484 670 }
485 671
486 672 if (!$redirectUrl) {
487 673 $redirectUrl = Helper::baseUrl();
@@ -507,9 +693,9 @@
507 693 $btnText = __('Continue to the community', 'fluent-community');
508 694
509 695 $html = '<div class="fcom_completed"><div class="fcom_complted_header"><h2>' . __('Welcome back!', 'fluent-community') . '</h2>';
510 696 $html .= '<p>' . __('You have successfully logged in to the community', 'fluent-community') . '</p></div>';
511 - $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>';
512 698 $html .= '</div>';
513 699
514 700 wp_send_json([
515 701 'success_html' => $html,
@@ -522,9 +708,10 @@
522 708 $portalSettings = Helper::generalSettings();
523 709 $isFluentAuth = AuthHelper::isFluentAuthAvailable();
524 710 $loginSettings = AuthenticationService::getFormattedAuthSettings('login');
525 711 $formSettings = Arr::get($loginSettings, 'form');
526 - $currentUrl = home_url(add_query_arg($_GET, $GLOBALS['wp']->request));
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 */
527 714 $title = sprintf(__('Login to %s', 'fluent-community'), Arr::get($portalSettings, 'site_title'));
528 715
529 716 $description = '';
530 717 if ($invitation) {
@@ -534,40 +721,62 @@
534 721 if ($space) {
535 722 $title = $space->title . ' - ' . Arr::get($portalSettings, 'site_title');
536 723 }
537 724 }
538 - $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 + });
539 734 }
540 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 +
541 749 if ($isFluentAuth) {
542 - add_filter('login_form_top', function () {
543 - $reditectUrl = Arr::get($_GET, 'redirect_to');
750 + add_filter('login_form_top', function () use ($invitation) {
751 + $reditectUrl = Arr::get($_GET, 'redirect_to'); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
544 752 if (!$reditectUrl) {
545 - $reditectUrl = apply_filters('fluent_community/default_redirect_url', $reditectUrl);
753 + $reditectUrl = apply_filters('fluent_community/default_redirect_url', Helper::baseUrl());
546 754 }
547 755 ob_start();
548 756 ?>
757 + <?php if ($invitation) { ?>
758 + <input type="hidden" name="invitation_token" value="<?php echo esc_attr($invitation->message_rendered); ?>"/>
759 + <?php } ?>
549 760 <input name="is_fcom_auth" type="hidden" value="yes"/>
550 761 <input type="hidden" name="fcom_redirect" value="<?php echo esc_url($reditectUrl); ?>"/>
551 762 <?php
552 763 return ob_get_clean();
553 764 });
554 -
555 765 ?>
556 766 <div id="fcom_user_onboard_wrap" class="fcom_user_onboard">
557 767 <div class="fcom_onboard_header">
768 + <?php do_action('fluent_community/before_auth_form_header', 'login'); ?>
558 769 <div class="fcom_onboard_header_title">
559 - <?php if (!empty($formSettings['title'])): ?>
560 - <h2 style="color: <?php echo esc_attr($formSettings['title_color']); ?>;">
561 - <?php echo esc_html($formSettings['title']); ?>
562 - </h2>
563 - <?php endif; ?>
770 + <?php if (!empty($formSettings['title'])): ?>
771 + <h2>
772 + <?php echo esc_html($formSettings['title']); ?>
773 + </h2>
774 + <?php endif; ?>
564 775 </div>
565 776 <?php if (!empty($formSettings['description'])): ?>
566 777 <div class="fcom_onboard_sub">
567 - <p style="color: <?php echo esc_attr($formSettings['text_color']); ?>;">
568 - <?php echo wp_kses_post($formSettings['description']); ?>
569 - </p>
778 + <?php echo wp_kses_post(trim($formSettings['description'])); ?>
570 779 </div>
571 780 <?php endif; ?>
572 781 </div>
573 782 <div class="fcom_onboard_body">
@@ -573,19 +782,19 @@
573 782 <div class="fcom_onboard_body">
574 783 <div class="fcom_onboard_form">
575 784 <?php echo do_shortcode('[fluent_auth_login redirect_to="' . esc_url($currentUrl) . '"]'); ?>
576 785 <div class="fcom_spaced_divider">
577 - <?php if (AuthHelper::isRegistrationEnabled()): ?>
786 + <?php if ($invitation || AuthHelper::isRegistrationEnabled()): ?>
578 787 <div class="fcom_alt_auth_text">
579 - <?php _e('Don\'t have an account?', 'fluent-community'); ?>
580 - <a href="<?php echo esc_url(add_query_arg('form', 'register', $currentUrl)); ?>">
581 - <?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'); ?>
582 791 </a>
583 792 </div>
584 793 <?php endif; ?>
585 794 <p class="fcom_reset_pass_text">
586 - <a href="<?php echo esc_url(add_query_arg('form', 'reset_password', $currentUrl)); ?>">
587 - <?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'); ?>
588 797 </a>
589 798 </p>
590 799 </div>
591 800 </div>
@@ -596,11 +805,14 @@
596 805 }
597 806
598 807 $frameData['redirect'] = $currentUrl;
599 808
600 - $frameData['hiddenFields'] = [];
809 + $frameData['hiddenFields'] = [
810 + 'action' => 'fcom_user_login_form',
811 + '_fcom_login_nonce' => wp_create_nonce('fcom_user_login_nonce'),
812 + ];
601 813 if ($invitation) {
602 - $frameData['loginBtnText'] = __('Log In & Accept Invitation', 'fluent-community');
814 + $frameData['button_label'] = __('Log In & Accept Invitation', 'fluent-community');
603 815 $frameData['hiddenFields']['invitation_token'] = $invitation->message_rendered;
604 816 }
605 817
606 818 $frameData['title'] = $title;
@@ -609,16 +821,16 @@
609 821 $frameData['defaults'] = [
610 822 'email' => $invitation ? $invitation->message : ''
611 823 ];
612 824
613 - if (AuthHelper::isRegistrationEnabled()) {
614 - $frameData['signupUrl'] = add_query_arg('form', 'register', $currentUrl);
825 + if ($invitation || AuthHelper::isRegistrationEnabled()) {
826 + $frameData['signupUrl'] = $signupUrl;
615 827 }
616 828
617 829 $frameData['settings'] = $formSettings;
618 830
619 - if (isset($_GET['redirect_to'])) {
620 - $frameData['redirect_to'] = sanitize_url($_GET['redirect_to']);
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
621 833 }
622 834
623 835 App::make('view')->render('auth.login_form', $frameData);
624 836 }
@@ -626,8 +838,28 @@
626 838 public function renderRegistrationForm($frameData, $invitation = null)
627 839 {
628 840 $formFields = AuthHelper::getFormFields($invitation);
629 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 +
630 862 $frameData['formFields'] = $formFields;
631 863
632 864 if ($invitation) {
633 865 $frameData['hiddenFields'] = [
@@ -632,21 +864,50 @@
632 864 if ($invitation) {
633 865 $frameData['hiddenFields'] = [
634 866 'invitation_token' => $invitation->message_rendered,
635 867 'action' => 'fcom_user_registration',
636 - '_fls_signup_nonce' => wp_create_nonce('fluent_auth_signup_nonce')
868 + '_fcom_signup_nonce' => wp_create_nonce('fluent_auth_signup_nonce')
637 869 ];
638 870
639 871 $invitationBy = $invitation->xprofile ? $invitation->xprofile->display_name : __('Someone', 'fluent-community');
640 - $frameData['description'] = sprintf(__('%s has invited you to join this community. Please create an account to accept your invitation.', 'fluent-community'), $invitationBy);
641 - $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');
642 884 } else {
643 885 $frameData['hiddenFields'] = [
644 886 'register' => 'yes',
645 887 'action' => 'fcom_user_registration',
646 - '_fls_signup_nonce' => wp_create_nonce('fluent_auth_signup_nonce')
888 + '_fcom_signup_nonce' => wp_create_nonce('fluent_auth_signup_nonce'),
647 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 + }
648 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 + });
649 910
650 911 App::make('view')->render('auth.user_invitation', $frameData);
651 912 }
652 913 }