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/AuthHelper.php +210 -46 1.1.02.10.01 View file →
@@ -14,9 +14,9 @@
14 14 $errors = new \WP_Error();
15 15
16 16 $sanitized_user_login = sanitize_user($user_login);
17 17
18 - $user_email = apply_filters('user_registration_email', $user_email);
18 + $user_email = apply_filters('user_registration_email', $user_email); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
19 19
20 20 // Check the username.
21 21 if ('' === $sanitized_user_login) {
22 22 $errors->add('empty_username', __('<strong>Error</strong>: Please enter a username.', 'fluent-community'));
@@ -26,9 +26,9 @@
26 26 } elseif (username_exists($sanitized_user_login)) {
27 27 $errors->add('username_exists', __('<strong>Error</strong>: This username is already registered. Please choose another one.', 'fluent-community'));
28 28 } else {
29 29 /** This filter is documented in wp-includes/user.php */
30 - $illegal_user_logins = (array)apply_filters('illegal_user_logins', array());
30 + $illegal_user_logins = (array)apply_filters('illegal_user_logins', array()); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
31 31 if (in_array(strtolower($sanitized_user_login), array_map('strtolower', $illegal_user_logins), true)) {
32 32 $errors->add('invalid_username', __('<strong>Error</strong>: Sorry, that username is not allowed.', 'fluent-community'));
33 33 }
34 34 }
@@ -45,12 +45,20 @@
45 45 __('<strong>Error:</strong> This email address is already registered. Please login or try resetting your password.', 'fluent-community')
46 46 );
47 47 }
48 48
49 - do_action('register_post', $sanitized_user_login, $user_email, $errors);
49 + /**
50 + * MemberPress rejects every `register_post` while its "Disable WordPress registration form"
51 + * option is on (default on). That option targets wp-login.php, not the community portal, which has its own registration gate.
52 + */
53 + $hadMeprBlocker = remove_action('register_post', 'MeprUsersCtrl::maybe_disable_wp_registration_form', 10);
50 54
51 - $errors = apply_filters('registration_errors', $errors, $sanitized_user_login, $user_email);
55 + do_action('register_post', $sanitized_user_login, $user_email, $errors); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
52 56
57 + if ($hadMeprBlocker) {
58 + add_action('register_post', 'MeprUsersCtrl::maybe_disable_wp_registration_form', 10, 3);
59 + }
60 +
53 61 if ($errors->has_errors()) {
54 62 return $errors;
55 63 }
56 64
@@ -104,15 +112,15 @@
104 112 return $errors;
105 113 }
106 114
107 115 if (!empty($_COOKIE['wp_lang'])) {
108 - $wp_lang = sanitize_text_field($_COOKIE['wp_lang']);
116 + $wp_lang = sanitize_text_field(wp_unslash($_COOKIE['wp_lang']));
109 117 if (in_array($wp_lang, get_available_languages(), true)) {
110 118 update_user_meta($user_id, 'locale', $wp_lang); // Set user locale if defined on registration.
111 119 }
112 120 }
113 121
114 - do_action('register_new_user', $user_id);
122 + do_action('register_new_user', $user_id); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
115 123
116 124 return $user_id;
117 125 }
118 126
@@ -124,9 +132,9 @@
124 132
125 133 $user = get_user_by('ID', $user->ID);
126 134
127 135 if ($user) {
128 - do_action('wp_login', $user->user_login, $user);
136 + do_action('wp_login', $user->user_login, $user); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
129 137 }
130 138
131 139 return $user;
132 140 }
@@ -139,20 +147,24 @@
139 147
140 148 return false;
141 149 }
142 150
143 - public static function getFormFields($invitation = null)
151 + public static function getTermsText()
144 152 {
145 -
146 153 $policyUrl = apply_filters('fluent_community/terms_policy_url', get_privacy_policy_url());
147 154
148 155 $termsText = __('I agree to the terms and conditions', 'fluent-community');
149 - if($policyUrl) {
150 - $termsText = sprintf(__('I agree to the %1sterms and conditions%2s', 'fluent-community'), '<a rel="nooppener" href="' . esc_url($policyUrl) . '" target="_blank">', '</a>');
156 + if ($policyUrl) {
157 + /* translators: %1$s is replaced by the text "terms and conditions", %2$s is replaced by the text "to the terms and conditions" */
158 + $termsText = sprintf(__('I agree to the %1$s terms and conditions %2$s', 'fluent-community'), '<a rel="noopener" href="' . esc_url($policyUrl) . '" target="_blank">', '</a>');
151 159 }
152 160
161 + return $termsText;
162 + }
153 163
154 - $fields = apply_filters('fluent_communuty/auth/signup_fields', [
164 + public static function getFormFields($invitation = null)
165 + {
166 + $fields = apply_filters('fluent_community/auth/signup_fields', [
155 167 'full_name' => [
156 168 'label' => __('Full name', 'fluent-community'),
157 169 'placeholder' => __('Your first & last name', 'fluent-community'),
158 170 'type' => 'text',
@@ -165,9 +177,9 @@
165 177 'placeholder' => __('Your email address', 'fluent-community'),
166 178 'label' => __('Email Address', 'fluent-community'),
167 179 'required' => true,
168 180 'value' => $invitation ? $invitation->message : '',
169 - 'readonly' => !!$invitation,
181 + 'readonly' => $invitation && $invitation->message,
170 182 'sanitize_callback' => 'sanitize_email'
171 183 ],
172 184 'username' => [
173 185 'type' => 'text',
@@ -191,13 +203,13 @@
191 203 'sanitize_callback' => 'sanitize_text_field'
192 204 ],
193 205 'terms' => [
194 206 'type' => 'inline_checkbox',
195 - 'inline_label' => $termsText,
207 + 'inline_label' => self::getTermsText(),
196 208 'required' => true
197 209 ]
198 210 ], $invitation);
199 -
211 +
200 212 if (!self::isPasswordConfRequired()) {
201 213 unset($fields['conf_password']);
202 214 }
203 215
@@ -203,8 +215,21 @@
203 215
204 216 return $fields;
205 217 }
206 218
219 + public static function getLostPasswordUrl($redirectUrl = '')
220 + {
221 + if (self::isFluentAuthAvailable()) {
222 + $url = add_query_arg([
223 + 'form' => 'reset_password'
224 + ], Helper::getAuthUrl());
225 + } else {
226 + $url = wp_lostpassword_url($redirectUrl);;
227 + }
228 +
229 + return apply_filters('fluent_community/auth/lost_password_url', $url);
230 + }
231 +
207 232 public static function getLoginFormFields()
208 233 {
209 234 return apply_filters('fluent_community/auth/login_fields', [
210 235 'username' => [
@@ -222,22 +247,35 @@
222 247 'sanitize_callback' => 'sanitize_text_field'
223 248 ]
224 249 ]);
225 250 }
226 -
251 +
227 252 public static function isPasswordConfRequired()
228 253 {
229 - return apply_filters('fluent_community/autg/password_confirmation', true);
254 + $isRequired = apply_filters_deprecated('fluent_community/autg/password_confirmation', [true], '2.7.8', 'fluent_community/auth/password_confirmation');
255 +
256 + return apply_filters('fluent_community/auth/password_confirmation', $isRequired);
230 257 }
231 258
232 259 public static function isRegistrationEnabled()
233 260 {
234 - return apply_filters('fluent_community/auth/registration_enabled', !!get_option('users_can_register'));
261 +
262 + $enabled = !!get_option('users_can_register');
263 +
264 + if (!$enabled) {
265 + $generalSettinsg = Helper::generalSettings();
266 + $enabled = $generalSettinsg['explicit_registration'] !== 'no';
267 + }
268 +
269 + return apply_filters('fluent_community/auth/registration_enabled', $enabled);
235 270 }
236 271
237 272 public static function isTwoFactorEnabled()
238 273 {
239 - return apply_filters('fluent_community/auth/two_factor_enabled', true);
274 + // fluent_auth/verify_signup_email is kept for backward compatibility with FluentAuth-targeted snippets
275 + $enabled = apply_filters('fluent_auth/verify_signup_email', true);
276 +
277 + return apply_filters('fluent_community/auth/two_factor_enabled', $enabled);
240 278 }
241 279
242 280 public static function get2FaRegistrationCodeForm($formData)
243 281 {
@@ -242,34 +280,33 @@
242 280 public static function get2FaRegistrationCodeForm($formData)
243 281 {
244 282 $generalSettings = Helper::generalSettings();
245 283 try {
246 - $verifcationCode = str_pad(random_int(100123, 900987), 6, 0, STR_PAD_LEFT);
284 + $verifcationCode = str_pad((string) random_int(100123, 900987), 6, '0', STR_PAD_LEFT);
247 285 } catch (\Exception $e) {
248 - $verifcationCode = str_pad(mt_rand(100123, 900987), 6, 0, STR_PAD_LEFT);
286 + $verifcationCode = str_pad((string) wp_rand(100123, 900987), 6, '0', STR_PAD_LEFT);
249 287 }
250 288
251 - // Hash the code
289 + // Keep the code hash server-side, keyed by an opaque challenge id. The client only ever
290 + // receives the id, never the password verifier, so the code cannot be recovered offline.
252 291 $codeHash = wp_hash_password($verifcationCode);
253 -
254 - // Create a token with the email and code hash
255 - $data = [
292 + $signedToken = 'fcs_' . wp_generate_password(40, false);
293 + set_transient('fcom_signup_2fa_' . $signedToken, [
256 294 'email' => $formData['email'],
257 295 'code_hash' => $codeHash,
258 - 'expires' => time() + 600 // 10 minutes expiry
259 - ];
260 - $token = base64_encode(json_encode($data));
296 + 'expires' => time() + 600, // 10 minutes expiry
297 + 'attempts' => 0,
298 + ], 600);
261 299
262 - // Sign the token
263 - $signature = hash_hmac('sha256', $token, SECURE_AUTH_KEY);
264 - $signedToken = $token . '.' . $signature;
265 -
300 + /* translators: %s is replaced by the title of the site */
266 301 $mailSubject = apply_filters("fluent_community/auth/signup_verification_mail_subject", sprintf(__('Your registration verification code for %s', 'fluent-community'), Arr::get($generalSettings, 'site_title')));
267 302
268 303 $pStart = '<p style="font-family: Arial, sans-serif; font-size: 16px; font-weight: normal; margin: 0; margin-bottom: 16px;">';
269 304
305 + /* translators: %s is replaced by the name of the user */
270 306 $message = $pStart . sprintf(__('Hello %s,', 'fluent-community'), Arr::get($formData, 'first_name')) . '</p>' .
271 307 $pStart . __('Thank you for registering with us! To complete the setup of your account, please enter the verification code below on the registration page.', 'fluent-community') . '</p>' .
308 + /* translators: %s is replaced by the verification code */
272 309 $pStart . '<b>' . sprintf(__('Verification Code: %s', 'fluent-community'), $verifcationCode) . '</b></p>' .
273 310 '<br />' .
274 311 $pStart . __('This code is valid for 10 minutes and is meant to ensure the security of your account. If you did not initiate this request, please ignore this email.', 'fluent-community') . '</p>';
275 312
@@ -284,9 +321,10 @@
284 321 'bodyContent' => $message,
285 322 'pre_header' => __('Activate your account', 'fluent-community'),
286 323 'footerLines' => [
287 324 __('If you did not initiate this request, please ignore this email.', 'fluent-community'),
288 - sprintf(__('This email has been sent from %1$s. Site: %2$s', 'fluent-community'), Arr::get($generalSettings, 'site_title'), site_url())
325 + /* translators: %1$s is replaced by the title of the site, %2$s is replaced by the home URL */
326 + sprintf(__('This email has been sent from %1$s. Site: %2$s', 'fluent-community'), Arr::get($generalSettings, 'site_title'), home_url())
289 327 ]
290 328 ]);
291 329
292 330 $mailer = new Mailer($formData['email'], $mailSubject, $message);
@@ -301,23 +339,35 @@
301 339 ob_start();
302 340 ?>
303 341 <div class="fls_signup_verification">
304 342 <input type="hidden" name="__two_fa_signed_token" value="<?php echo esc_attr($signedToken); ?>"/>
305 - <p><?php echo esc_html(sprintf(__('A verification code has been sent to %s. Please provide the code below: ', 'fluent-community'), $formData['email'])) ?></p>
306 - <div class="fcom_form-group fcom_field_vefication">
343 + <?php /* translators: %s is replaced by the email address */ ?>
344 + <p><?php echo esc_html(\sprintf(__('A verification code has been sent to %s. Please provide the code below: ', 'fluent-community'), $formData['email'])) ?></p>
345 + <div class="fcom_form-group fcom_field_verification">
307 346 <div class="fcom_form_label">
308 - <label for="fcom_field_vefication"><?php _e('Verification Code', 'fluent-community'); ?></label>
347 + <label for="fcom_field_verification"><?php esc_html_e('Verification Code', 'fluent-community'); ?></label>
309 348 </div>
310 349 <div class="fs_input_wrap">
311 - <input type="text" id="fcom_field_vefication"
312 - placeholder="<?php _e('2FA Code', 'fluent-community'); ?>" name="_email_verification_code"
350 + <input type="text" id="fcom_field_verification"
351 + placeholder="<?php esc_html_e('2FA Code', 'fluent-community'); ?>" name="_email_verification_code"
313 352 required/>
314 353 </div>
315 354 </div>
316 355 <div class="fcom_form-group">
317 356 <div class="fcom_form_input">
318 - <button type="submit" class="fcom_btn fcom_btn_primary">
319 - <?php _e('Complete Signup', 'fluent-community'); ?>
357 + <button type="submit" class="fcom_btn has_svg_loader fcom_btn_primary">
358 + <svg version="1.1" class="fls_loading_svg" x="0px" y="0px" width="40px" height="20px" viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
359 + <path fill="currentColor" d="M43.935,25.145c0-10.318-8.364-18.683-18.683-18.683c-10.318,0-18.683,8.365-18.683,18.683h4.068c0-8.071,6.543-14.615,14.615-14.615c8.072,0,14.615,6.543,14.615,14.615H43.935z">
360 + <animateTransform attributeType="xml"
361 + attributeName="transform"
362 + type="rotate"
363 + from="0 25 25"
364 + to="360 25 25"
365 + dur="0.6s"
366 + repeatCount="indefinite"/>
367 + </path>
368 + </svg>
369 + <span> <?php esc_html_e('Complete Signup', 'fluent-community'); ?></span>
320 370 </button>
321 371 </div>
322 372 </div>
323 373 </div>
@@ -327,28 +377,43 @@
327 377 }
328 378
329 379 public static function validateVerificationCode($code, $verificationToken, $formData)
330 380 {
331 - list($data, $signature) = explode('.', $verificationToken, 2);
332 - $expectedSignature = hash_hmac('sha256', $data, SECURE_AUTH_KEY);
381 + if (!is_string($verificationToken) || $verificationToken === '') {
382 + return new \WP_Error('invalid_token', __('Invalid verification token. Please try again', 'fluent-community'));
383 + }
333 384
334 - if (!hash_equals($expectedSignature, $signature)) {
385 + $transientKey = 'fcom_signup_2fa_' . $verificationToken;
386 + $data = get_transient($transientKey);
387 +
388 + if (!is_array($data) || empty($data['expires']) || empty($data['email']) || empty($data['code_hash'])) {
335 389 return new \WP_Error('invalid_token', __('Invalid verification token. Please try again', 'fluent-community'));
336 390 }
337 391
338 - $data = json_decode(base64_decode($data), true);
339 - if ($data['expires'] < time()) {
392 + if ((int)$data['expires'] < time()) {
393 + delete_transient($transientKey);
340 394 return new \WP_Error('expired_token', __('Verification token has expired. Please try again.', 'fluent-community'));
341 395 }
342 396
343 - if ($data['email'] !== $formData['email']) {
397 + if (!isset($formData['email']) || $data['email'] !== $formData['email']) {
344 398 return new \WP_Error('invalid_email', __('Invalid email address. Please try again', 'fluent-community'));
345 399 }
346 400
401 + // Cap online guesses per challenge: after too many wrong codes the challenge is burned.
402 + if ((int) Arr::get($data, 'attempts', 0) >= 10) {
403 + delete_transient($transientKey);
404 + return new \WP_Error('too_many_attempts', __('Too many invalid attempts. Please try again', 'fluent-community'));
405 + }
406 +
347 407 if (!wp_check_password($code, $data['code_hash'])) {
408 + $data['attempts'] = (int) Arr::get($data, 'attempts', 0) + 1;
409 + set_transient($transientKey, $data, max(1, (int) $data['expires'] - time()));
348 410 return new \WP_Error('invalid_code', __('Invalid verification code. Please try again', 'fluent-community'));
349 411 }
350 412
413 + // Single-use: consume the challenge on success.
414 + delete_transient($transientKey);
415 +
351 416 return true;
352 417 }
353 418
354 419 public static function isAuthRateLimit()
@@ -370,6 +435,105 @@
370 435
371 436 $rateLimit = $rateLimit + 1;
372 437 set_transient($transientKey, $rateLimit, 300); // per 5 minutes
373 438 return true;
439 + }
440 +
441 +
442 + public static function nativeLoginForm($args = array(), $hiddenFields = [])
443 + {
444 + $defaults = array(
445 + 'echo' => true,
446 + 'redirect' => (is_ssl() ? 'https://' : 'http://')
447 + . (isset($_SERVER['HTTP_HOST']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_HOST'])) : '')
448 + . (isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : ''),
449 + 'form_id' => 'loginform',
450 + 'label_username' => __('Email Address', 'fluent-community'),
451 + 'label_password' => __('Password', 'fluent-community'),
452 + 'label_remember' => __('Remember Me', 'fluent-community'),
453 + 'label_log_in' => __('Log In', 'fluent-community'),
454 + 'id_username' => 'user_login',
455 + 'id_password' => 'user_pass',
456 + 'id_remember' => 'rememberme',
457 + 'id_submit' => 'wp-submit',
458 + 'remember' => true,
459 + 'value_username' => '',
460 + 'username_placeholder' => __('Your account email address', 'fluent-community'),
461 + 'password_placeholder' => __('Your account password', 'fluent-community'),
462 + 'value_remember' => false,
463 + );
464 +
465 + $args = wp_parse_args($args, apply_filters('login_form_defaults', $defaults)); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
466 +
467 + $login_form_top = apply_filters('login_form_top', '', $args); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
468 +
469 + $login_form_middle = apply_filters('login_form_middle', '', $args); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
470 +
471 + $login_form_bottom = apply_filters('login_form_bottom', '', $args); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
472 +
473 + $actionUrl = esc_url(site_url('wp-login.php', 'login_post'));
474 +
475 + if (isset($args['action_url'])) {
476 + $actionUrl = esc_url($args['action_url']);
477 + }
478 +
479 + foreach ($hiddenFields as $key => $value) {
480 + $login_form_top .= \sprintf(
481 + '<input type="hidden" name="%1$s" value="%2$s" />',
482 + esc_attr($key),
483 + esc_attr($value)
484 + );
485 + }
486 +
487 + $form = \sprintf(
488 + '<form name="%1$s" id="%1$s" action="%2$s" method="post">',
489 + esc_attr($args['form_id']),
490 + $actionUrl
491 + ) .
492 + $login_form_top .
493 + \sprintf(
494 + '<p class="login-username fcom_form-group">
495 + <label for="%1$s">%2$s</label>
496 + <input type="text" name="log" id="%1$s" autocomplete="username" class="input" value="%3$s" placeholder="%4$s" size="20" />
497 + </p>',
498 + esc_attr($args['id_username']),
499 + esc_html($args['label_username']),
500 + esc_attr($args['value_username']),
501 + esc_attr($args['username_placeholder']),
502 + ) .
503 + \sprintf(
504 + '<p class="login-password fcom_form-group">
505 + <label for="%1$s">%2$s</label>
506 + <input type="password" name="pwd" id="%1$s" autocomplete="current-password" placeholder="%3$s" class="input" value="" size="20" />
507 + </p>',
508 + esc_attr($args['id_password']),
509 + esc_html($args['label_password']),
510 + esc_attr($args['password_placeholder'])
511 + ) .
512 + $login_form_middle .
513 + ($args['remember'] ?
514 + \sprintf(
515 + '<p class="login-remember fcom_form-group"><label><input name="rememberme" type="checkbox" id="%1$s" value="forever"%2$s /> %3$s</label></p>',
516 + esc_attr($args['id_remember']),
517 + ($args['value_remember'] ? ' checked="checked"' : ''),
518 + esc_html($args['label_remember'])
519 + ) : ''
520 + ) .
521 + \sprintf(
522 + '<p class="login-submit">
523 + <input type="submit" name="wp-submit" id="%1$s" class="button button-primary" value="%2$s" />
524 + <input type="hidden" name="redirect_to" value="%3$s" />
525 + </p>',
526 + esc_attr($args['id_submit']),
527 + esc_attr($args['label_log_in']),
528 + esc_url($args['redirect'])
529 + ) .
530 + $login_form_bottom .
531 + '</form>';
532 +
533 + if ($args['echo']) {
534 + echo $form; // @phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
535 + } else {
536 + return $form;
537 + }
374 538 }
375 539 }