| @@ -56,11 +56,17 @@ | ||
| 56 | 56 | * @param array $formData |
| 57 | 57 | */ |
| 58 | 58 | do_action('fluent_support/before_signup_validation', $formData); |
| 59 | 59 | |
| 60 | - $checkRecaptchaAvailability = $this->isRecaptchaApplicable('signup_form'); | |
| 61 | - if ($checkRecaptchaAvailability && !$formData['_email_verification_hash']) { | |
| 62 | - $validateCaptcha = ReCaptchaHandler::validateRecaptcha($formData['g-recaptcha-response']); | |
| 60 | + // the verification code is only submitted on the second step, after the customer has | |
| 61 | + // already passed the captcha and received the email; that step validates the hash | |
| 62 | + // against an issued record below, so the captcha can be skipped there. The first | |
| 63 | + // submit (which sends the verification email) always has to pass the captcha. | |
| 64 | + $isVerificationStep = !empty($formData['_email_verification_token']); | |
| 65 | + | |
| 66 | + $checkRecaptchaAvailability = ReCaptchaHandler::isRecaptchaApplicable('signup_form'); | |
| 67 | + if ($checkRecaptchaAvailability && !$isVerificationStep) { | |
| 68 | + $validateCaptcha = ReCaptchaHandler::validateRecaptcha($formData['g-recaptcha-response'] ?? '', null, null, 'signup'); | |
| 63 | 69 | if (!$validateCaptcha) { |
| 64 | 70 | return $this->response([ |
| 65 | 71 | 'message' => __('Your recaptcha is not verified', 'fluent-support') |
| 66 | 72 | ], 422); |
| @@ -68,9 +74,9 @@ | ||
| 68 | 74 | } |
| 69 | 75 | |
| 70 | 76 | $this->validate($formData, $rules, $messages); |
| 71 | 77 | |
| 72 | - if (empty($formData['_email_verification_token'])) { | |
| 78 | + if (!$isVerificationStep) { | |
| 73 | 79 | $tokenHtml = EmailVerificationHandler::sendSignupEmailVerificationHtml($formData); |
| 74 | 80 | |
| 75 | 81 | return $this->response([ |
| 76 | 82 | 'verification_html' => $tokenHtml |
| @@ -76,9 +82,9 @@ | ||
| 76 | 82 | 'verification_html' => $tokenHtml |
| 77 | 83 | ]); |
| 78 | 84 | } else { |
| 79 | 85 | $token = $formData['_email_verification_token']; |
| 80 | - $verificationHash = $formData['_email_verification_hash']; | |
| 86 | + $verificationHash = $formData['_email_verification_hash'] ?? ''; | |
| 81 | 87 | |
| 82 | 88 | $logHashMeta = Meta::where('object_type', 'fs_login_hashes',) |
| 83 | 89 | ->where('key', $verificationHash) |
| 84 | 90 | ->first(); |
| @@ -112,10 +118,15 @@ | ||
| 112 | 118 | ], 422); |
| 113 | 119 | } |
| 114 | 120 | |
| 115 | 121 | // check if it got expired or not |
| 122 | + // valid_till is written via gmdate() on a current_time('timestamp') basis (always | |
| 123 | + // UTC-equivalent, since WP resets the runtime timezone to UTC on every request), so | |
| 124 | + // it must be parsed back as UTC here - otherwise strtotime() would silently reinterpret | |
| 125 | + // it under whatever timezone a plugin/theme may have switched to via | |
| 126 | + // date_default_timezone_set() without restoring it, causing false expiry (or non-expiry) | |
| 116 | 127 | $validTill = $logHash['valid_till'] ?? ''; |
| 117 | - if (($logHash['used_count'] ?? 0) > 5 || ($validTill && strtotime($validTill) < current_time('timestamp'))) { | |
| 128 | + if (($logHash['used_count'] ?? 0) > 5 || ($validTill && strtotime($validTill . ' UTC') < current_time('timestamp'))) { | |
| 118 | 129 | wp_send_json([ |
| 119 | 130 | 'message' => __('Your verification code has been expired. Please try again', 'fluent-support') |
| 120 | 131 | ], 422); |
| 121 | 132 | } |
| @@ -221,11 +232,11 @@ | ||
| 221 | 232 | } |
| 222 | 233 | |
| 223 | 234 | $data = $request->all(); |
| 224 | 235 | |
| 225 | - $checkRecaptchaAvailability = $this->isRecaptchaApplicable('login_form'); | |
| 236 | + $checkRecaptchaAvailability = ReCaptchaHandler::isRecaptchaApplicable('login_form'); | |
| 226 | 237 | if ($checkRecaptchaAvailability) { |
| 227 | - $validateCaptcha = ReCaptchaHandler::validateRecaptcha($data['g-recaptcha-response']); | |
| 238 | + $validateCaptcha = ReCaptchaHandler::validateRecaptcha($data['g-recaptcha-response'] ?? '', null, null, 'login'); | |
| 228 | 239 | |
| 229 | 240 | if (!$validateCaptcha) { |
| 230 | 241 | return $this->response([ |
| 231 | 242 | 'message' => __('Your recaptcha is not verified', 'fluent-support') |
| @@ -349,26 +360,8 @@ | ||
| 349 | 360 | set_transient($rateLimitKey, $attempts + 1, 15 * MINUTE_IN_SECONDS); |
| 350 | 361 | } |
| 351 | 362 | } |
| 352 | 363 | |
| 353 | - public function isRecaptchaApplicable($formName) | |
| 354 | - { | |
| 355 | - $reCaptchaSettingsData = Meta::where('object_type', '_fs_recaptcha_settings')->first(); | |
| 356 | - if(!isset($reCaptchaSettingsData->value)){ | |
| 357 | - return false; | |
| 358 | - } | |
| 359 | - $reCaptchaData = Helper::safeUnserialize($reCaptchaSettingsData->value, []); | |
| 360 | - if(!isset($reCaptchaData['is_enabled']) || !isset($reCaptchaData['formContainingReCaptcha'])){ | |
| 361 | - return false; | |
| 362 | - } | |
| 363 | - $isEnabled = filter_var($reCaptchaData['is_enabled'], FILTER_VALIDATE_BOOLEAN); | |
| 364 | - if (!$isEnabled) { | |
| 365 | - return false; | |
| 366 | - } | |
| 367 | - $formContainingReCaptcha = $reCaptchaData['formContainingReCaptcha']; | |
| 368 | - return 'yes' === $formContainingReCaptcha[$formName]; | |
| 369 | - } | |
| 370 | - | |
| 371 | 364 | private function nativeLoginHandler($user, $info, $redirectUrl = '') |
| 372 | 365 | { |
| 373 | 366 | if (!$redirectUrl) { |
| 374 | 367 | $redirectUrl = Helper::getPortalBaseUrl(); |
| @@ -507,11 +500,9 @@ | ||
| 507 | 500 | $user_data = get_user_by('login', $usernameOrEmail); |
| 508 | 501 | } |
| 509 | 502 | |
| 510 | 503 | if (!$user_data) { |
| 511 | - return $this->sendError([ | |
| 512 | - 'message' => __('Invalid username or email', 'fluent-support') | |
| 513 | - ]); | |
| 504 | + return $this->sendResetPassResponse(); | |
| 514 | 505 | } |
| 515 | 506 | |
| 516 | 507 | $user_data = apply_filters('lostpassword_user_data', $user_data, $errors); |
| 517 | 508 | |
| @@ -519,24 +510,17 @@ | ||
| 519 | 510 | |
| 520 | 511 | $errors = apply_filters('lostpassword_errors', $errors, $user_data); |
| 521 | 512 | |
| 522 | 513 | if ($errors->has_errors()) { |
| 523 | - return $this->sendError([ | |
| 524 | - 'message' => $errors->get_error_message() | |
| 525 | - ]); | |
| 514 | + return $this->sendResetPassResponse(); | |
| 526 | 515 | } |
| 527 | 516 | |
| 528 | 517 | if (!$user_data) { |
| 529 | - return $this->sendError([ | |
| 530 | - 'message' => __('There is no account with that username or email address.', 'fluent-support') | |
| 531 | - ]); | |
| 518 | + return $this->sendResetPassResponse(); | |
| 532 | 519 | } |
| 533 | 520 | |
| 534 | 521 | if (is_multisite() && !is_user_member_of_blog($user_data->ID, get_current_blog_id())) { |
| 535 | - | |
| 536 | - return $this->sendError([ | |
| 537 | - 'message' => __('Invalid username or email', 'fluent-support') | |
| 538 | - ]); | |
| 522 | + return $this->sendResetPassResponse(); | |
| 539 | 523 | } |
| 540 | 524 | |
| 541 | 525 | // Redefining user_login ensures we return the right case in the email. |
| 542 | 526 | $user_login = $user_data->user_login; |
| @@ -544,21 +528,13 @@ | ||
| 544 | 528 | do_action('retrieve_password', $user_login); |
| 545 | 529 | |
| 546 | 530 | $allow = apply_filters('allow_password_reset', true, $user_data->ID); |
| 547 | 531 | |
| 548 | - if (!$allow) { | |
| 549 | - return $this->sendError([ | |
| 550 | - 'message' => __('Password reset is not allowed for this user', 'fluent-support') | |
| 551 | - ]); | |
| 532 | + if (!$allow || is_wp_error($allow)) { | |
| 533 | + return $this->sendResetPassResponse(); | |
| 552 | 534 | } |
| 553 | 535 | |
| 554 | - if (is_wp_error($allow)) { | |
| 555 | - return $this->sendError([ | |
| 556 | - 'message' => $allow->get_error_message() | |
| 557 | - ]); | |
| 558 | - } | |
| 559 | 536 | |
| 560 | - | |
| 561 | 537 | /* |
| 562 | 538 | * Filter reset password link text |
| 563 | 539 | * |
| 564 | 540 | * @since v1.5.7 |
| @@ -574,11 +550,9 @@ | ||
| 574 | 550 | // first send has already put a working link in that inbox. |
| 575 | 551 | $cooldownKey = 'fs_reset_pass_sent_' . wp_hash($user_data->ID); |
| 576 | 552 | |
| 577 | 553 | if (get_transient($cooldownKey)) { |
| 578 | - return $this->sendError([ | |
| 579 | - 'message' => __('A password reset link was already sent to this account recently. Please check your email, including the spam folder, or try again in a few minutes.', 'fluent-support') | |
| 580 | - ], 429); | |
| 554 | + return $this->sendResetPassResponse(); | |
| 581 | 555 | } |
| 582 | 556 | |
| 583 | 557 | set_transient($cooldownKey, 1, 5 * MINUTE_IN_SECONDS); |
| 584 | 558 | |
| @@ -620,10 +594,24 @@ | ||
| 620 | 594 | $headers = array('Content-Type: text/html; charset=UTF-8'); |
| 621 | 595 | |
| 622 | 596 | wp_mail($user_data->user_email, $mailSubject, $message, $headers); |
| 623 | 597 | |
| 598 | + return $this->sendResetPassResponse(); | |
| 599 | + } | |
| 600 | + | |
| 601 | + /** | |
| 602 | + * Single response for every password reset outcome. | |
| 603 | + * | |
| 604 | + * Whether the account exists, is disallowed, is not a member of this site or is | |
| 605 | + * inside the resend cooldown, the caller sees the same thing — otherwise the form | |
| 606 | + * confirms which usernames and email addresses are real. | |
| 607 | + * | |
| 608 | + * @return mixed | |
| 609 | + */ | |
| 610 | + protected function sendResetPassResponse() | |
| 611 | + { | |
| 624 | 612 | return $this->sendSuccess([ |
| 625 | - 'message' => __('Please check your email for the reset link', 'fluent-support') | |
| 613 | + 'message' => __('If an account matches that username or email, a password reset link has been sent. Please check your inbox, including the spam folder.', 'fluent-support') | |
| 626 | 614 | ]); |
| 627 | 615 | } |
| 628 | 616 | |
| 629 | 617 | /** |