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