| @@ -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,26 +132,112 @@ | ||
| 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 | } |
| 133 | 141 | |
| 142 | + /** | |
| 143 | + * The slug this plugin is known by inside FluentAuth. | |
| 144 | + */ | |
| 145 | + const FLUENT_AUTH_HOST = 'fluent-community'; | |
| 146 | + | |
| 147 | + /** | |
| 148 | + * Whether FluentAuth's login stack is usable on this screen. | |
| 149 | + * | |
| 150 | + * Note the order this has to be asked in: adoptFluentAuth() is what makes the answer | |
| 151 | + * yes on a site whose shortcode setting is off, so the auth screen adopts first and | |
| 152 | + * asks second. Everywhere else - a plugin wondering whether the portal is running | |
| 153 | + * FluentAuth's forms - the question stands on its own. | |
| 154 | + */ | |
| 134 | 155 | public static function isFluentAuthAvailable() |
| 135 | 156 | { |
| 136 | - if (defined('FLUENT_AUTH_VERSION') && FLUENT_AUTH_VERSION) { | |
| 137 | - return (new \FluentAuth\App\Hooks\Handlers\CustomAuthHandler())->isEnabled(); | |
| 157 | + if (!defined('FLUENT_AUTH_VERSION') || !FLUENT_AUTH_VERSION) { | |
| 158 | + return false; | |
| 138 | 159 | } |
| 139 | 160 | |
| 140 | - return false; | |
| 161 | + return (new \FluentAuth\App\Hooks\Handlers\CustomAuthHandler())->isEnabled(); | |
| 141 | 162 | } |
| 142 | 163 | |
| 164 | + /** | |
| 165 | + * Tells FluentAuth this plugin exists, so it recognises the admin-ajax posts our | |
| 166 | + * auth screen makes later. Cheap enough to run on every request, which is what it | |
| 167 | + * has to do - the form post is a request of its own. | |
| 168 | + * | |
| 169 | + * @return void | |
| 170 | + */ | |
| 171 | + public static function registerWithFluentAuth() | |
| 172 | + { | |
| 173 | + if (!self::hasFluentAuthBridge()) { | |
| 174 | + return; | |
| 175 | + } | |
| 176 | + | |
| 177 | + \FluentAuth\App\Services\LoginBridge::register( | |
| 178 | + self::FLUENT_AUTH_HOST, | |
| 179 | + 'is_fcom_auth', | |
| 180 | + /* | |
| 181 | + * Our own login endpoint. Unreachable while the portal renders FluentAuth's | |
| 182 | + * form, but a site that filters the adoption back off falls through to it, | |
| 183 | + * and this is what keeps the second factor arriving as a form there rather | |
| 184 | + * than as the error message handleUserLogin() would otherwise print. | |
| 185 | + */ | |
| 186 | + ['fcom_user_login_form'] | |
| 187 | + ); | |
| 188 | + } | |
| 189 | + | |
| 190 | + /** | |
| 191 | + * Hands the screen being rendered to FluentAuth: its shortcodes render here even | |
| 192 | + * where the site has the front end forms switched off, its assets load, and its | |
| 193 | + * endpoints answer the posts this screen's forms make. | |
| 194 | + * | |
| 195 | + * @return bool whether FluentAuth took it | |
| 196 | + */ | |
| 197 | + public static function adoptFluentAuth() | |
| 198 | + { | |
| 199 | + if (!self::hasFluentAuthBridge()) { | |
| 200 | + return false; | |
| 201 | + } | |
| 202 | + | |
| 203 | + \FluentAuth\App\Services\LoginBridge::adopt([ | |
| 204 | + 'host' => self::FLUENT_AUTH_HOST | |
| 205 | + ]); | |
| 206 | + | |
| 207 | + return true; | |
| 208 | + } | |
| 209 | + | |
| 210 | + /** | |
| 211 | + * @return bool whether the installed FluentAuth is new enough to be adopted | |
| 212 | + */ | |
| 213 | + private static function hasFluentAuthBridge() | |
| 214 | + { | |
| 215 | + // FluentAuth's autoloader requires the mapped file unconditionally, so probing | |
| 216 | + // for a class an older release does not ship is a fatal error, not a false. | |
| 217 | + return defined('FLUENT_AUTH_VERSION') | |
| 218 | + && FLUENT_AUTH_VERSION | |
| 219 | + && defined('FLUENT_AUTH_PLUGIN_PATH') | |
| 220 | + && file_exists(FLUENT_AUTH_PLUGIN_PATH . 'app/Services/LoginBridge.php') | |
| 221 | + && class_exists('\FluentAuth\App\Services\LoginBridge'); | |
| 222 | + } | |
| 223 | + | |
| 224 | + public static function getTermsText() | |
| 225 | + { | |
| 226 | + $policyUrl = apply_filters('fluent_community/terms_policy_url', get_privacy_policy_url()); | |
| 227 | + | |
| 228 | + $termsText = __('I agree to the terms and conditions', 'fluent-community'); | |
| 229 | + if ($policyUrl) { | |
| 230 | + /* translators: %1$s is replaced by the text "terms and conditions", %2$s is replaced by the text "to the terms and conditions" */ | |
| 231 | + $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>'); | |
| 232 | + } | |
| 233 | + | |
| 234 | + return $termsText; | |
| 235 | + } | |
| 236 | + | |
| 143 | 237 | public static function getFormFields($invitation = null) |
| 144 | 238 | { |
| 145 | - $fields = apply_filters('fluent_communuty/auth/signup_fields', [ | |
| 239 | + $fields = apply_filters('fluent_community/auth/signup_fields', [ | |
| 146 | 240 | 'full_name' => [ |
| 147 | 241 | 'label' => __('Full name', 'fluent-community'), |
| 148 | 242 | 'placeholder' => __('Your first & last name', 'fluent-community'), |
| 149 | 243 | 'type' => 'text', |
| @@ -156,15 +250,15 @@ | ||
| 156 | 250 | 'placeholder' => __('Your email address', 'fluent-community'), |
| 157 | 251 | 'label' => __('Email Address', 'fluent-community'), |
| 158 | 252 | 'required' => true, |
| 159 | 253 | 'value' => $invitation ? $invitation->message : '', |
| 160 | - 'readonly' => !!$invitation, | |
| 254 | + 'readonly' => $invitation && $invitation->message, | |
| 161 | 255 | 'sanitize_callback' => 'sanitize_email' |
| 162 | 256 | ], |
| 163 | 257 | 'username' => [ |
| 164 | 258 | 'type' => 'text', |
| 165 | 259 | 'placeholder' => __('No space or special characters', 'fluent-community'), |
| 166 | - 'label' => __('Space username', 'fluent-community'), | |
| 260 | + 'label' => __('Username', 'fluent-community'), | |
| 167 | 261 | 'required' => true, |
| 168 | 262 | 'sanitize_callback' => 'sanitize_user' |
| 169 | 263 | ], |
| 170 | 264 | 'password' => [ |
| @@ -182,9 +276,9 @@ | ||
| 182 | 276 | 'sanitize_callback' => 'sanitize_text_field' |
| 183 | 277 | ], |
| 184 | 278 | 'terms' => [ |
| 185 | 279 | 'type' => 'inline_checkbox', |
| 186 | - 'inline_label' => __('I agree to the terms and conditions', 'fluent-community'), | |
| 280 | + 'inline_label' => self::getTermsText(), | |
| 187 | 281 | 'required' => true |
| 188 | 282 | ] |
| 189 | 283 | ], $invitation); |
| 190 | 284 | |
| @@ -194,21 +288,67 @@ | ||
| 194 | 288 | |
| 195 | 289 | return $fields; |
| 196 | 290 | } |
| 197 | 291 | |
| 292 | + public static function getLostPasswordUrl($redirectUrl = '') | |
| 293 | + { | |
| 294 | + if (self::isFluentAuthAvailable()) { | |
| 295 | + $url = add_query_arg([ | |
| 296 | + 'form' => 'reset_password' | |
| 297 | + ], Helper::getAuthUrl()); | |
| 298 | + } else { | |
| 299 | + $url = wp_lostpassword_url($redirectUrl);; | |
| 300 | + } | |
| 301 | + | |
| 302 | + return apply_filters('fluent_community/auth/lost_password_url', $url); | |
| 303 | + } | |
| 304 | + | |
| 305 | + public static function getLoginFormFields() | |
| 306 | + { | |
| 307 | + return apply_filters('fluent_community/auth/login_fields', [ | |
| 308 | + 'username' => [ | |
| 309 | + 'type' => 'text', | |
| 310 | + 'placeholder' => __('Your account email address', 'fluent-community'), | |
| 311 | + 'label' => __('Email Address', 'fluent-community'), | |
| 312 | + 'required' => true, | |
| 313 | + 'sanitize_callback' => 'sanitize_user' | |
| 314 | + ], | |
| 315 | + 'password' => [ | |
| 316 | + 'type' => 'password', | |
| 317 | + 'placeholder' => __('Your account password', 'fluent-community'), | |
| 318 | + 'label' => __('Password', 'fluent-community'), | |
| 319 | + 'required' => true, | |
| 320 | + 'sanitize_callback' => 'sanitize_text_field' | |
| 321 | + ] | |
| 322 | + ]); | |
| 323 | + } | |
| 324 | + | |
| 198 | 325 | public static function isPasswordConfRequired() |
| 199 | 326 | { |
| 200 | - return apply_filters('fluent_community/autg/password_confirmation', true); | |
| 327 | + $isRequired = apply_filters_deprecated('fluent_community/autg/password_confirmation', [true], '2.7.8', 'fluent_community/auth/password_confirmation'); | |
| 328 | + | |
| 329 | + return apply_filters('fluent_community/auth/password_confirmation', $isRequired); | |
| 201 | 330 | } |
| 202 | 331 | |
| 203 | 332 | public static function isRegistrationEnabled() |
| 204 | 333 | { |
| 205 | - return apply_filters('fluent_community/auth/registration_enabled', !!get_option('users_can_register')); | |
| 334 | + | |
| 335 | + $enabled = !!get_option('users_can_register'); | |
| 336 | + | |
| 337 | + if (!$enabled) { | |
| 338 | + $generalSettinsg = Helper::generalSettings(); | |
| 339 | + $enabled = $generalSettinsg['explicit_registration'] !== 'no'; | |
| 340 | + } | |
| 341 | + | |
| 342 | + return apply_filters('fluent_community/auth/registration_enabled', $enabled); | |
| 206 | 343 | } |
| 207 | 344 | |
| 208 | 345 | public static function isTwoFactorEnabled() |
| 209 | 346 | { |
| 210 | - return apply_filters('fluent_community/auth/two_factor_enabled', true); | |
| 347 | + // fluent_auth/verify_signup_email is kept for backward compatibility with FluentAuth-targeted snippets | |
| 348 | + $enabled = apply_filters('fluent_auth/verify_signup_email', true); | |
| 349 | + | |
| 350 | + return apply_filters('fluent_community/auth/two_factor_enabled', $enabled); | |
| 211 | 351 | } |
| 212 | 352 | |
| 213 | 353 | public static function get2FaRegistrationCodeForm($formData) |
| 214 | 354 | { |
| @@ -213,34 +353,33 @@ | ||
| 213 | 353 | public static function get2FaRegistrationCodeForm($formData) |
| 214 | 354 | { |
| 215 | 355 | $generalSettings = Helper::generalSettings(); |
| 216 | 356 | try { |
| 217 | - $verifcationCode = str_pad(random_int(100123, 900987), 6, 0, STR_PAD_LEFT); | |
| 357 | + $verifcationCode = str_pad((string) random_int(100123, 900987), 6, '0', STR_PAD_LEFT); | |
| 218 | 358 | } catch (\Exception $e) { |
| 219 | - $verifcationCode = str_pad(mt_rand(100123, 900987), 6, 0, STR_PAD_LEFT); | |
| 359 | + $verifcationCode = str_pad((string) wp_rand(100123, 900987), 6, '0', STR_PAD_LEFT); | |
| 220 | 360 | } |
| 221 | 361 | |
| 222 | - // Hash the code | |
| 362 | + // Keep the code hash server-side, keyed by an opaque challenge id. The client only ever | |
| 363 | + // receives the id, never the password verifier, so the code cannot be recovered offline. | |
| 223 | 364 | $codeHash = wp_hash_password($verifcationCode); |
| 224 | - | |
| 225 | - // Create a token with the email and code hash | |
| 226 | - $data = [ | |
| 365 | + $signedToken = 'fcs_' . wp_generate_password(40, false); | |
| 366 | + set_transient('fcom_signup_2fa_' . $signedToken, [ | |
| 227 | 367 | 'email' => $formData['email'], |
| 228 | 368 | 'code_hash' => $codeHash, |
| 229 | - 'expires' => time() + 600 // 10 minutes expiry | |
| 230 | - ]; | |
| 231 | - $token = base64_encode(json_encode($data)); | |
| 369 | + 'expires' => time() + 600, // 10 minutes expiry | |
| 370 | + 'attempts' => 0, | |
| 371 | + ], 600); | |
| 232 | 372 | |
| 233 | - // Sign the token | |
| 234 | - $signature = hash_hmac('sha256', $token, SECURE_AUTH_KEY); | |
| 235 | - $signedToken = $token . '.' . $signature; | |
| 236 | - | |
| 373 | + /* translators: %s is replaced by the title of the site */ | |
| 237 | 374 | $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 | 375 | |
| 239 | 376 | $pStart = '<p style="font-family: Arial, sans-serif; font-size: 16px; font-weight: normal; margin: 0; margin-bottom: 16px;">'; |
| 240 | 377 | |
| 378 | + /* translators: %s is replaced by the name of the user */ | |
| 241 | 379 | $message = $pStart . sprintf(__('Hello %s,', 'fluent-community'), Arr::get($formData, 'first_name')) . '</p>' . |
| 242 | 380 | $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>' . |
| 381 | + /* translators: %s is replaced by the verification code */ | |
| 243 | 382 | $pStart . '<b>' . sprintf(__('Verification Code: %s', 'fluent-community'), $verifcationCode) . '</b></p>' . |
| 244 | 383 | '<br />' . |
| 245 | 384 | $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 | 385 | |
| @@ -255,9 +394,10 @@ | ||
| 255 | 394 | 'bodyContent' => $message, |
| 256 | 395 | 'pre_header' => __('Activate your account', 'fluent-community'), |
| 257 | 396 | 'footerLines' => [ |
| 258 | 397 | __('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()) | |
| 398 | + /* translators: %1$s is replaced by the title of the site, %2$s is replaced by the home URL */ | |
| 399 | + sprintf(__('This email has been sent from %1$s. Site: %2$s', 'fluent-community'), Arr::get($generalSettings, 'site_title'), home_url()) | |
| 260 | 400 | ] |
| 261 | 401 | ]); |
| 262 | 402 | |
| 263 | 403 | $mailer = new Mailer($formData['email'], $mailSubject, $message); |
| @@ -272,23 +412,35 @@ | ||
| 272 | 412 | ob_start(); |
| 273 | 413 | ?> |
| 274 | 414 | <div class="fls_signup_verification"> |
| 275 | 415 | <input type="hidden" name="__two_fa_signed_token" value="<?php echo esc_attr($signedToken); ?>"/> |
| 276 | - <p><?php echo esc_html(sprintf(__('A verification code has been sent to %s. Please provide the code below: ', 'fluent-community'), $formData['email'])) ?></p> | |
| 277 | - <div class="fcom_form-group fcom_field_vefication"> | |
| 416 | + <?php /* translators: %s is replaced by the email address */ ?> | |
| 417 | + <p><?php echo esc_html(\sprintf(__('A verification code has been sent to %s. Please provide the code below: ', 'fluent-community'), $formData['email'])) ?></p> | |
| 418 | + <div class="fcom_form-group fcom_field_verification"> | |
| 278 | 419 | <div class="fcom_form_label"> |
| 279 | - <label for="fcom_field_vefication"><?php _e('Verification Code', 'fluent-community'); ?></label> | |
| 420 | + <label for="fcom_field_verification"><?php esc_html_e('Verification Code', 'fluent-community'); ?></label> | |
| 280 | 421 | </div> |
| 281 | 422 | <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" | |
| 423 | + <input type="text" id="fcom_field_verification" | |
| 424 | + placeholder="<?php esc_html_e('2FA Code', 'fluent-community'); ?>" name="_email_verification_code" | |
| 284 | 425 | required/> |
| 285 | 426 | </div> |
| 286 | 427 | </div> |
| 287 | 428 | <div class="fcom_form-group"> |
| 288 | 429 | <div class="fcom_form_input"> |
| 289 | - <button type="submit" class="fcom_btn fcom_btn_primary"> | |
| 290 | - <?php _e('Complete Signup', 'fluent-community'); ?> | |
| 430 | + <button type="submit" class="fcom_btn has_svg_loader fcom_btn_primary"> | |
| 431 | + <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"> | |
| 432 | + <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"> | |
| 433 | + <animateTransform attributeType="xml" | |
| 434 | + attributeName="transform" | |
| 435 | + type="rotate" | |
| 436 | + from="0 25 25" | |
| 437 | + to="360 25 25" | |
| 438 | + dur="0.6s" | |
| 439 | + repeatCount="indefinite"/> | |
| 440 | + </path> | |
| 441 | + </svg> | |
| 442 | + <span> <?php esc_html_e('Complete Signup', 'fluent-community'); ?></span> | |
| 291 | 443 | </button> |
| 292 | 444 | </div> |
| 293 | 445 | </div> |
| 294 | 446 | </div> |
| @@ -298,28 +450,43 @@ | ||
| 298 | 450 | } |
| 299 | 451 | |
| 300 | 452 | public static function validateVerificationCode($code, $verificationToken, $formData) |
| 301 | 453 | { |
| 302 | - list($data, $signature) = explode('.', $verificationToken, 2); | |
| 303 | - $expectedSignature = hash_hmac('sha256', $data, SECURE_AUTH_KEY); | |
| 454 | + if (!is_string($verificationToken) || $verificationToken === '') { | |
| 455 | + return new \WP_Error('invalid_token', __('Invalid verification token. Please try again', 'fluent-community')); | |
| 456 | + } | |
| 304 | 457 | |
| 305 | - if (!hash_equals($expectedSignature, $signature)) { | |
| 458 | + $transientKey = 'fcom_signup_2fa_' . $verificationToken; | |
| 459 | + $data = get_transient($transientKey); | |
| 460 | + | |
| 461 | + if (!is_array($data) || empty($data['expires']) || empty($data['email']) || empty($data['code_hash'])) { | |
| 306 | 462 | return new \WP_Error('invalid_token', __('Invalid verification token. Please try again', 'fluent-community')); |
| 307 | 463 | } |
| 308 | 464 | |
| 309 | - $data = json_decode(base64_decode($data), true); | |
| 310 | - if ($data['expires'] < time()) { | |
| 465 | + if ((int)$data['expires'] < time()) { | |
| 466 | + delete_transient($transientKey); | |
| 311 | 467 | return new \WP_Error('expired_token', __('Verification token has expired. Please try again.', 'fluent-community')); |
| 312 | 468 | } |
| 313 | 469 | |
| 314 | - if ($data['email'] !== $formData['email']) { | |
| 470 | + if (!isset($formData['email']) || $data['email'] !== $formData['email']) { | |
| 315 | 471 | return new \WP_Error('invalid_email', __('Invalid email address. Please try again', 'fluent-community')); |
| 316 | 472 | } |
| 317 | 473 | |
| 474 | + // Cap online guesses per challenge: after too many wrong codes the challenge is burned. | |
| 475 | + if ((int) Arr::get($data, 'attempts', 0) >= 10) { | |
| 476 | + delete_transient($transientKey); | |
| 477 | + return new \WP_Error('too_many_attempts', __('Too many invalid attempts. Please try again', 'fluent-community')); | |
| 478 | + } | |
| 479 | + | |
| 318 | 480 | if (!wp_check_password($code, $data['code_hash'])) { |
| 481 | + $data['attempts'] = (int) Arr::get($data, 'attempts', 0) + 1; | |
| 482 | + set_transient($transientKey, $data, max(1, (int) $data['expires'] - time())); | |
| 319 | 483 | return new \WP_Error('invalid_code', __('Invalid verification code. Please try again', 'fluent-community')); |
| 320 | 484 | } |
| 321 | 485 | |
| 486 | + // Single-use: consume the challenge on success. | |
| 487 | + delete_transient($transientKey); | |
| 488 | + | |
| 322 | 489 | return true; |
| 323 | 490 | } |
| 324 | 491 | |
| 325 | 492 | public static function isAuthRateLimit() |
| @@ -341,6 +508,105 @@ | ||
| 341 | 508 | |
| 342 | 509 | $rateLimit = $rateLimit + 1; |
| 343 | 510 | set_transient($transientKey, $rateLimit, 300); // per 5 minutes |
| 344 | 511 | return true; |
| 512 | + } | |
| 513 | + | |
| 514 | + | |
| 515 | + public static function nativeLoginForm($args = array(), $hiddenFields = []) | |
| 516 | + { | |
| 517 | + $defaults = array( | |
| 518 | + 'echo' => true, | |
| 519 | + 'redirect' => (is_ssl() ? 'https://' : 'http://') | |
| 520 | + . (isset($_SERVER['HTTP_HOST']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_HOST'])) : '') | |
| 521 | + . (isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : ''), | |
| 522 | + 'form_id' => 'loginform', | |
| 523 | + 'label_username' => __('Email Address', 'fluent-community'), | |
| 524 | + 'label_password' => __('Password', 'fluent-community'), | |
| 525 | + 'label_remember' => __('Remember Me', 'fluent-community'), | |
| 526 | + 'label_log_in' => __('Log In', 'fluent-community'), | |
| 527 | + 'id_username' => 'user_login', | |
| 528 | + 'id_password' => 'user_pass', | |
| 529 | + 'id_remember' => 'rememberme', | |
| 530 | + 'id_submit' => 'wp-submit', | |
| 531 | + 'remember' => true, | |
| 532 | + 'value_username' => '', | |
| 533 | + 'username_placeholder' => __('Your account email address', 'fluent-community'), | |
| 534 | + 'password_placeholder' => __('Your account password', 'fluent-community'), | |
| 535 | + 'value_remember' => false, | |
| 536 | + ); | |
| 537 | + | |
| 538 | + $args = wp_parse_args($args, apply_filters('login_form_defaults', $defaults)); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound | |
| 539 | + | |
| 540 | + $login_form_top = apply_filters('login_form_top', '', $args); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound | |
| 541 | + | |
| 542 | + $login_form_middle = apply_filters('login_form_middle', '', $args); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound | |
| 543 | + | |
| 544 | + $login_form_bottom = apply_filters('login_form_bottom', '', $args); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound | |
| 545 | + | |
| 546 | + $actionUrl = esc_url(site_url('wp-login.php', 'login_post')); | |
| 547 | + | |
| 548 | + if (isset($args['action_url'])) { | |
| 549 | + $actionUrl = esc_url($args['action_url']); | |
| 550 | + } | |
| 551 | + | |
| 552 | + foreach ($hiddenFields as $key => $value) { | |
| 553 | + $login_form_top .= \sprintf( | |
| 554 | + '<input type="hidden" name="%1$s" value="%2$s" />', | |
| 555 | + esc_attr($key), | |
| 556 | + esc_attr($value) | |
| 557 | + ); | |
| 558 | + } | |
| 559 | + | |
| 560 | + $form = \sprintf( | |
| 561 | + '<form name="%1$s" id="%1$s" action="%2$s" method="post">', | |
| 562 | + esc_attr($args['form_id']), | |
| 563 | + $actionUrl | |
| 564 | + ) . | |
| 565 | + $login_form_top . | |
| 566 | + \sprintf( | |
| 567 | + '<p class="login-username fcom_form-group"> | |
| 568 | + <label for="%1$s">%2$s</label> | |
| 569 | + <input type="text" name="log" id="%1$s" autocomplete="username" class="input" value="%3$s" placeholder="%4$s" size="20" /> | |
| 570 | + </p>', | |
| 571 | + esc_attr($args['id_username']), | |
| 572 | + esc_html($args['label_username']), | |
| 573 | + esc_attr($args['value_username']), | |
| 574 | + esc_attr($args['username_placeholder']), | |
| 575 | + ) . | |
| 576 | + \sprintf( | |
| 577 | + '<p class="login-password fcom_form-group"> | |
| 578 | + <label for="%1$s">%2$s</label> | |
| 579 | + <input type="password" name="pwd" id="%1$s" autocomplete="current-password" placeholder="%3$s" class="input" value="" size="20" /> | |
| 580 | + </p>', | |
| 581 | + esc_attr($args['id_password']), | |
| 582 | + esc_html($args['label_password']), | |
| 583 | + esc_attr($args['password_placeholder']) | |
| 584 | + ) . | |
| 585 | + $login_form_middle . | |
| 586 | + ($args['remember'] ? | |
| 587 | + \sprintf( | |
| 588 | + '<p class="login-remember fcom_form-group"><label><input name="rememberme" type="checkbox" id="%1$s" value="forever"%2$s /> %3$s</label></p>', | |
| 589 | + esc_attr($args['id_remember']), | |
| 590 | + ($args['value_remember'] ? ' checked="checked"' : ''), | |
| 591 | + esc_html($args['label_remember']) | |
| 592 | + ) : '' | |
| 593 | + ) . | |
| 594 | + \sprintf( | |
| 595 | + '<p class="login-submit"> | |
| 596 | + <input type="submit" name="wp-submit" id="%1$s" class="button button-primary" value="%2$s" /> | |
| 597 | + <input type="hidden" name="redirect_to" value="%3$s" /> | |
| 598 | + </p>', | |
| 599 | + esc_attr($args['id_submit']), | |
| 600 | + esc_attr($args['label_log_in']), | |
| 601 | + esc_url($args['redirect']) | |
| 602 | + ) . | |
| 603 | + $login_form_bottom . | |
| 604 | + '</form>'; | |
| 605 | + | |
| 606 | + if ($args['echo']) { | |
| 607 | + echo $form; // @phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped | |
| 608 | + } else { | |
| 609 | + return $form; | |
| 610 | + } | |
| 345 | 611 | } |
| 346 | 612 | } |