| @@ -24,11 +24,71 @@ | ||
| 24 | 24 | add_action('wp_ajax_yatra_ajax_login', [$this, 'handleAjaxLogin']); |
| 25 | 25 | |
| 26 | 26 | // Register AJAX action for non-logged-in users |
| 27 | 27 | add_action('wp_ajax_nopriv_yatra_ajax_login', [$this, 'handleAjaxLogin']); |
| 28 | + | |
| 29 | + // Password reset request (delegates to WordPress core's reset flow). | |
| 30 | + add_action('wp_ajax_yatra_ajax_lost_password', [$this, 'handleAjaxLostPassword']); | |
| 31 | + add_action('wp_ajax_nopriv_yatra_ajax_lost_password', [$this, 'handleAjaxLostPassword']); | |
| 28 | 32 | } |
| 29 | 33 | |
| 30 | 34 | /** |
| 35 | + * Handle AJAX password-reset request. | |
| 36 | + * | |
| 37 | + * This is a thin, on-site entry point that delegates to WordPress core's | |
| 38 | + * retrieve_password(): core generates the reset key and sends its standard | |
| 39 | + * reset email. We deliberately do NOT reimplement reset-token crypto. A | |
| 40 | + * generic success message is always returned to avoid account enumeration. | |
| 41 | + */ | |
| 42 | + public function handleAjaxLostPassword(): void | |
| 43 | + { | |
| 44 | + // Rate limiting (shared with login attempts). | |
| 45 | + $this->checkRateLimit(); | |
| 46 | + | |
| 47 | + // Verify nonce (same nonces the login form issues). | |
| 48 | + $nonce = $_POST['yatra_login_nonce'] ?? $_POST['nonce'] ?? ''; | |
| 49 | + if (!wp_verify_nonce($nonce, 'yatra_login_action') && !wp_verify_nonce($nonce, 'yatra_login_nonce')) { | |
| 50 | + wp_send_json_error([ | |
| 51 | + 'success' => false, | |
| 52 | + 'code' => 'security_check_failed', | |
| 53 | + 'message' => __('Security check failed. Please refresh the page and try again.', 'yatra'), | |
| 54 | + ]); | |
| 55 | + } | |
| 56 | + | |
| 57 | + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { | |
| 58 | + wp_send_json_error([ | |
| 59 | + 'success' => false, | |
| 60 | + 'code' => 'invalid_method', | |
| 61 | + 'message' => __('Invalid request method.', 'yatra'), | |
| 62 | + ]); | |
| 63 | + } | |
| 64 | + | |
| 65 | + $user_login = sanitize_text_field(wp_unslash($_POST['user_login'] ?? $_POST['log'] ?? '')); | |
| 66 | + | |
| 67 | + if ($user_login === '') { | |
| 68 | + wp_send_json_error([ | |
| 69 | + 'success' => false, | |
| 70 | + 'code' => 'empty_user_login', | |
| 71 | + 'message' => __('Please enter your email address or username.', 'yatra'), | |
| 72 | + ]); | |
| 73 | + } | |
| 74 | + | |
| 75 | + // Populate $_POST['user_login'] for cross-version compatibility: | |
| 76 | + // retrieve_password() reads it directly on WordPress older than 5.7, | |
| 77 | + // and the explicit argument is used on 5.7+ (extra args are ignored on | |
| 78 | + // older cores, so this is safe either way). | |
| 79 | + $_POST['user_login'] = $user_login; | |
| 80 | + retrieve_password($user_login); | |
| 81 | + | |
| 82 | + // Always report success regardless of whether the account exists — | |
| 83 | + // prevents user/email enumeration (mirrors WordPress core behavior). | |
| 84 | + wp_send_json_success([ | |
| 85 | + 'success' => true, | |
| 86 | + 'message' => __('If an account exists for that email, a password reset link has been sent. Please check your inbox.', 'yatra'), | |
| 87 | + ]); | |
| 88 | + } | |
| 89 | + | |
| 90 | + /** | |
| 31 | 91 | * Handle AJAX login request |
| 32 | 92 | */ |
| 33 | 93 | public function handleAjaxLogin(): void |
| 34 | 94 | { |
| @@ -110,23 +170,38 @@ | ||
| 110 | 170 | */ |
| 111 | 171 | private function checkRateLimit(): void |
| 112 | 172 | { |
| 113 | 173 | $ip = $this->getClientIp(); |
| 114 | - $transient_key = 'yatra_login_limit_' . md5($ip); | |
| 115 | - $attempts = get_transient($transient_key) ?: 0; | |
| 174 | + $enabled = (bool) apply_filters('yatra_login_rate_limit_enabled', true, $ip); | |
| 175 | + if (!$enabled) { | |
| 176 | + return; | |
| 177 | + } | |
| 178 | + | |
| 179 | + // Defaults: 10 attempts per 1 minute (can be overridden via filters). | |
| 180 | + $maxAttempts = (int) apply_filters('yatra_login_rate_limit_max_attempts', 10, $ip); | |
| 181 | + $windowSeconds = (int) apply_filters('yatra_login_rate_limit_window_seconds', MINUTE_IN_SECONDS, $ip); | |
| 182 | + $transient_key = (string) apply_filters('yatra_login_rate_limit_transient_key', 'yatra_login_limit_' . md5($ip), $ip); | |
| 183 | + | |
| 184 | + $attempts = (int) (get_transient($transient_key) ?: 0); | |
| 116 | 185 | |
| 117 | - // Allow 5 attempts per 15 minutes | |
| 118 | - if ($attempts >= 5) { | |
| 186 | + // Allow N attempts per window | |
| 187 | + if ($maxAttempts > 0 && $attempts >= $maxAttempts) { | |
| 119 | 188 | $this->logSecurityEvent('rate_limit_exceeded', ['ip' => $ip]); |
| 189 | + $minutes = (int) max(1, ceil(max(1, $windowSeconds) / 60)); | |
| 120 | 190 | wp_send_json_error([ |
| 121 | 191 | 'success' => false, |
| 122 | 192 | 'code' => 'rate_limit_exceeded', |
| 123 | - 'message' => __('Too many login attempts. Please try again in 15 minutes.', 'yatra') | |
| 193 | + 'message' => sprintf( | |
| 194 | + /* translators: %d = minutes to wait */ | |
| 195 | + __('Too many login attempts. Please try again in %d minute(s).', 'yatra'), | |
| 196 | + $minutes | |
| 197 | + ), | |
| 124 | 198 | ]); |
| 125 | 199 | } |
| 126 | 200 | |
| 127 | 201 | // Increment counter |
| 128 | - set_transient($transient_key, $attempts + 1, 15 * MINUTE_IN_SECONDS); | |
| 202 | + $ttl = $windowSeconds > 0 ? $windowSeconds : MINUTE_IN_SECONDS; | |
| 203 | + set_transient($transient_key, $attempts + 1, $ttl); | |
| 129 | 204 | } |
| 130 | 205 | |
| 131 | 206 | /** |
| 132 | 207 | * Validate login input |
| @@ -207,9 +282,9 @@ | ||
| 207 | 282 | // Provide user-friendly error messages |
| 208 | 283 | if (in_array($error_code, ['invalid_username', 'incorrect_password'], true)) { |
| 209 | 284 | $error_message = __('Invalid username or password. Please try again.', 'yatra'); |
| 210 | 285 | } elseif ($error_code === 'email_not_verified') { |
| 211 | - $error_message = $error_message; // Use the specific message | |
| 286 | + // Keep the specific message from the error | |
| 212 | 287 | } else { |
| 213 | 288 | $error_message = __('Login failed. Please try again.', 'yatra'); |
| 214 | 289 | } |
| 215 | 290 | |
| @@ -261,9 +336,9 @@ | ||
| 261 | 336 | */ |
| 262 | 337 | private function validateRedirectUrl(string $redirect_url): string |
| 263 | 338 | { |
| 264 | 339 | if (empty($redirect_url)) { |
| 265 | - return home_url('/my-account'); | |
| 340 | + return home_url('/' . \Yatra\Services\SettingsService::getAccountBase()); | |
| 266 | 341 | } |
| 267 | 342 | |
| 268 | 343 | $redirect_url = sanitize_url($redirect_url); |
| 269 | 344 | |
| @@ -268,9 +343,9 @@ | ||
| 268 | 343 | $redirect_url = sanitize_url($redirect_url); |
| 269 | 344 | |
| 270 | 345 | // Validate URL is safe |
| 271 | 346 | if (!wp_http_validate_url($redirect_url)) { |
| 272 | - return home_url('/my-account'); | |
| 347 | + return home_url('/' . \Yatra\Services\SettingsService::getAccountBase()); | |
| 273 | 348 | } |
| 274 | 349 | |
| 275 | 350 | // Only allow redirects to same host |
| 276 | 351 | $redirect_host = parse_url($redirect_url, PHP_URL_HOST); |
| @@ -276,9 +351,9 @@ | ||
| 276 | 351 | $redirect_host = parse_url($redirect_url, PHP_URL_HOST); |
| 277 | 352 | $site_host = parse_url(home_url(), PHP_URL_HOST); |
| 278 | 353 | |
| 279 | 354 | if ($redirect_host !== $site_host) { |
| 280 | - return home_url('/my-account'); | |
| 355 | + return home_url('/' . \Yatra\Services\SettingsService::getAccountBase()); | |
| 281 | 356 | } |
| 282 | 357 | |
| 283 | 358 | return $redirect_url; |
| 284 | 359 | } |
| @@ -340,9 +415,8 @@ | ||
| 340 | 415 | 'ip' => $this->getClientIp(), |
| 341 | 416 | 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '' |
| 342 | 417 | ], $data); |
| 343 | 418 | |
| 344 | - error_log('Yatra Login Security: ' . json_encode($log_data)); | |
| 345 | 419 | } |
| 346 | 420 | |
| 347 | 421 | /** |
| 348 | 422 | * Log login attempts |
| @@ -352,7 +426,6 @@ | ||
| 352 | 426 | if (!defined('WP_DEBUG') || !WP_DEBUG) { |
| 353 | 427 | return; |
| 354 | 428 | } |
| 355 | 429 | |
| 356 | - error_log('Yatra Login Attempt: ' . $username . ' from IP: ' . $this->getClientIp()); | |
| 357 | 430 | } |
| 358 | 431 | } |