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 +236 -43 1.0.952.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 }
@@ -41,16 +41,24 @@
41 41 $user_email = '';
42 42 } elseif (email_exists($user_email)) {
43 43 $errors->add(
44 44 'email_exists',
45 - __('<strong>Error:</strong> This email address is already registered. Please login or try reset password', 'fluent-community')
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,11 +147,24 @@
139 147
140 148 return false;
141 149 }
142 150
151 + public static function getTermsText()
152 + {
153 + $policyUrl = apply_filters('fluent_community/terms_policy_url', get_privacy_policy_url());
154 +
155 + $termsText = __('I agree to the terms and conditions', 'fluent-community');
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>');
159 + }
160 +
161 + return $termsText;
162 + }
163 +
143 164 public static function getFormFields($invitation = null)
144 165 {
145 - $fields = apply_filters('fluent_communuty/auth/signup_fields', [
166 + $fields = apply_filters('fluent_community/auth/signup_fields', [
146 167 'full_name' => [
147 168 'label' => __('Full name', 'fluent-community'),
148 169 'placeholder' => __('Your first & last name', 'fluent-community'),
149 170 'type' => 'text',
@@ -156,15 +177,15 @@
156 177 'placeholder' => __('Your email address', 'fluent-community'),
157 178 'label' => __('Email Address', 'fluent-community'),
158 179 'required' => true,
159 180 'value' => $invitation ? $invitation->message : '',
160 - 'readonly' => !!$invitation,
181 + 'readonly' => $invitation && $invitation->message,
161 182 'sanitize_callback' => 'sanitize_email'
162 183 ],
163 184 'username' => [
164 185 'type' => 'text',
165 186 'placeholder' => __('No space or special characters', 'fluent-community'),
166 - 'label' => __('Space username', 'fluent-community'),
187 + 'label' => __('Username', 'fluent-community'),
167 188 'required' => true,
168 189 'sanitize_callback' => 'sanitize_user'
169 190 ],
170 191 'password' => [
@@ -182,9 +203,9 @@
182 203 'sanitize_callback' => 'sanitize_text_field'
183 204 ],
184 205 'terms' => [
185 206 'type' => 'inline_checkbox',
186 - 'inline_label' => __('I agree to the terms and conditions', 'fluent-community'),
207 + 'inline_label' => self::getTermsText(),
187 208 'required' => true
188 209 ]
189 210 ], $invitation);
190 211
@@ -194,21 +215,67 @@
194 215
195 216 return $fields;
196 217 }
197 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 +
232 + public static function getLoginFormFields()
233 + {
234 + return apply_filters('fluent_community/auth/login_fields', [
235 + 'username' => [
236 + 'type' => 'text',
237 + 'placeholder' => __('Your account email address', 'fluent-community'),
238 + 'label' => __('Email Address', 'fluent-community'),
239 + 'required' => true,
240 + 'sanitize_callback' => 'sanitize_user'
241 + ],
242 + 'password' => [
243 + 'type' => 'password',
244 + 'placeholder' => __('Your account password', 'fluent-community'),
245 + 'label' => __('Password', 'fluent-community'),
246 + 'required' => true,
247 + 'sanitize_callback' => 'sanitize_text_field'
248 + ]
249 + ]);
250 + }
251 +
198 252 public static function isPasswordConfRequired()
199 253 {
200 - 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);
201 257 }
202 258
203 259 public static function isRegistrationEnabled()
204 260 {
205 - 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);
206 270 }
207 271
208 272 public static function isTwoFactorEnabled()
209 273 {
210 - 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);
211 278 }
212 279
213 280 public static function get2FaRegistrationCodeForm($formData)
214 281 {
@@ -213,34 +280,33 @@
213 280 public static function get2FaRegistrationCodeForm($formData)
214 281 {
215 282 $generalSettings = Helper::generalSettings();
216 283 try {
217 - $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);
218 285 } catch (\Exception $e) {
219 - $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);
220 287 }
221 288
222 - // 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.
223 291 $codeHash = wp_hash_password($verifcationCode);
224 -
225 - // Create a token with the email and code hash
226 - $data = [
292 + $signedToken = 'fcs_' . wp_generate_password(40, false);
293 + set_transient('fcom_signup_2fa_' . $signedToken, [
227 294 'email' => $formData['email'],
228 295 'code_hash' => $codeHash,
229 - 'expires' => time() + 600 // 10 minutes expiry
230 - ];
231 - $token = base64_encode(json_encode($data));
296 + 'expires' => time() + 600, // 10 minutes expiry
297 + 'attempts' => 0,
298 + ], 600);
232 299
233 - // Sign the token
234 - $signature = hash_hmac('sha256', $token, SECURE_AUTH_KEY);
235 - $signedToken = $token . '.' . $signature;
236 -
300 + /* translators: %s is replaced by the title of the site */
237 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')));
238 302
239 303 $pStart = '<p style="font-family: Arial, sans-serif; font-size: 16px; font-weight: normal; margin: 0; margin-bottom: 16px;">';
240 304
305 + /* translators: %s is replaced by the name of the user */
241 306 $message = $pStart . sprintf(__('Hello %s,', 'fluent-community'), Arr::get($formData, 'first_name')) . '</p>' .
242 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 */
243 309 $pStart . '<b>' . sprintf(__('Verification Code: %s', 'fluent-community'), $verifcationCode) . '</b></p>' .
244 310 '<br />' .
245 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>';
246 312
@@ -255,9 +321,10 @@
255 321 'bodyContent' => $message,
256 322 'pre_header' => __('Activate your account', 'fluent-community'),
257 323 'footerLines' => [
258 324 __('If you did not initiate this request, please ignore this email.', 'fluent-community'),
259 - 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())
260 327 ]
261 328 ]);
262 329
263 330 $mailer = new Mailer($formData['email'], $mailSubject, $message);
@@ -272,23 +339,35 @@
272 339 ob_start();
273 340 ?>
274 341 <div class="fls_signup_verification">
275 342 <input type="hidden" name="__two_fa_signed_token" value="<?php echo esc_attr($signedToken); ?>"/>
276 - <p><?php echo esc_html(sprintf(__('A verification code as been sent to %s. Please provide the code bellow: ', 'fluent-community'), $formData['email'])) ?></p>
277 - <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">
278 346 <div class="fcom_form_label">
279 - <label for="fcom_field_vefication"><?php _e('Vefication Code', 'fluent-community'); ?></label>
347 + <label for="fcom_field_verification"><?php esc_html_e('Verification Code', 'fluent-community'); ?></label>
280 348 </div>
281 349 <div class="fs_input_wrap">
282 - <input type="text" id="fcom_field_vefication"
283 - 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"
284 352 required/>
285 353 </div>
286 354 </div>
287 355 <div class="fcom_form-group">
288 356 <div class="fcom_form_input">
289 - <button type="submit" class="fcom_btn fcom_btn_primary">
290 - <?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>
291 370 </button>
292 371 </div>
293 372 </div>
294 373 </div>
@@ -298,28 +377,43 @@
298 377 }
299 378
300 379 public static function validateVerificationCode($code, $verificationToken, $formData)
301 380 {
302 - list($data, $signature) = explode('.', $verificationToken, 2);
303 - $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 + }
304 384
305 - 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'])) {
306 389 return new \WP_Error('invalid_token', __('Invalid verification token. Please try again', 'fluent-community'));
307 390 }
308 391
309 - $data = json_decode(base64_decode($data), true);
310 - if ($data['expires'] < time()) {
311 - return new \WP_Error('expired_token', __('Verification token has expired. Please try again', 'fluent-community'));
392 + if ((int)$data['expires'] < time()) {
393 + delete_transient($transientKey);
394 + return new \WP_Error('expired_token', __('Verification token has expired. Please try again.', 'fluent-community'));
312 395 }
313 396
314 - if ($data['email'] !== $formData['email']) {
397 + if (!isset($formData['email']) || $data['email'] !== $formData['email']) {
315 398 return new \WP_Error('invalid_email', __('Invalid email address. Please try again', 'fluent-community'));
316 399 }
317 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 +
318 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()));
319 410 return new \WP_Error('invalid_code', __('Invalid verification code. Please try again', 'fluent-community'));
320 411 }
321 412
413 + // Single-use: consume the challenge on success.
414 + delete_transient($transientKey);
415 +
322 416 return true;
323 417 }
324 418
325 419 public static function isAuthRateLimit()
@@ -341,6 +435,105 @@
341 435
342 436 $rateLimit = $rateLimit + 1;
343 437 set_transient($transientKey, $rateLimit, 300); // per 5 minutes
344 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 + }
345 538 }
346 539 }