PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / app / Ajax / LoginAjax.php

LoginAjax.php in Yatra – Travel Booking & Tour Operator Software 3.0.16, at app/Ajax/LoginAjax.php

432 lines 14.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Yatra\Ajax;
6
7 use WP_REST_Request;
8 use WP_REST_Response;
9 use WP_Error;
10
11 /**
12 * Login AJAX Handler
13 *
14 * Handles AJAX login requests
15 */
16 class LoginAjax
17 {
18 /**
19 * Constructor - Initialize AJAX hooks
20 */
21 public function __construct()
22 {
23 // Register AJAX action for logged-in users
24 add_action('wp_ajax_yatra_ajax_login', [$this, 'handleAjaxLogin']);
25
26 // Register AJAX action for non-logged-in users
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']);
32 }
33
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 /**
91 * Handle AJAX login request
92 */
93 public function handleAjaxLogin(): void
94 {
95 // Rate limiting check
96 $this->checkRateLimit();
97
98 // Verify nonce with multiple checks
99 $nonce = $_POST['yatra_login_nonce'] ?? $_POST['nonce'] ?? '';
100 if (!wp_verify_nonce($nonce, 'yatra_login_action') && !wp_verify_nonce($nonce, 'yatra_login_nonce')) {
101 $this->logSecurityEvent('nonce_verification_failed', [
102 'nonce' => substr($nonce, 0, 8) . '...',
103 'ip' => $this->getClientIp()
104 ]);
105
106 wp_send_json_error([
107 'success' => false,
108 'code' => 'security_check_failed',
109 'message' => __('Security check failed. Please refresh the page and try again.', 'yatra')
110 ]);
111 }
112
113 // Validate request method
114 if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
115 wp_send_json_error([
116 'success' => false,
117 'code' => 'invalid_method',
118 'message' => __('Invalid request method.', 'yatra')
119 ]);
120 }
121
122 // Get and sanitize form data
123 $username = sanitize_user($_POST['log'] ?? $_POST['username'] ?? '');
124 $password = $_POST['pwd'] ?? $_POST['password'] ?? '';
125 $remember = isset($_POST['rememberme']) && $_POST['rememberme'] === 'forever';
126 $redirect_to = $this->validateRedirectUrl($_POST['redirect_to'] ?? '');
127
128 // Enhanced input validation
129 $validation_result = $this->validateLoginInput($username, $password);
130 if (!$validation_result['valid']) {
131 wp_send_json_error([
132 'success' => false,
133 'code' => $validation_result['code'],
134 'message' => $validation_result['message']
135 ]);
136 }
137
138 // Log login attempt (security)
139 $this->logLoginAttempt($username);
140
141 // Attempt authentication with security checks
142 $user = $this->authenticateUser($username, $password);
143
144 if (is_wp_error($user)) {
145 $this->handleAuthenticationError($user);
146 return; // Exit early
147 }
148
149 // Additional security checks
150 if (!$this->isUserAllowedToLogin($user)) {
151 wp_send_json_error([
152 'success' => false,
153 'code' => 'user_not_allowed',
154 'message' => __('Your account is not allowed to login. Please contact support.', 'yatra')
155 ]);
156 }
157
158 // Successful login - set up secure session
159 $this->setupUserSession($user, $remember);
160
161 // Prepare secure success response
162 $response_data = $this->prepareSuccessResponse($user, $redirect_to);
163
164 // Send success response
165 wp_send_json_success($response_data);
166 }
167
168 /**
169 * Check rate limiting for login attempts
170 */
171 private function checkRateLimit(): void
172 {
173 $ip = $this->getClientIp();
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);
185
186 // Allow N attempts per window
187 if ($maxAttempts > 0 && $attempts >= $maxAttempts) {
188 $this->logSecurityEvent('rate_limit_exceeded', ['ip' => $ip]);
189 $minutes = (int) max(1, ceil(max(1, $windowSeconds) / 60));
190 wp_send_json_error([
191 'success' => false,
192 'code' => 'rate_limit_exceeded',
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 ),
198 ]);
199 }
200
201 // Increment counter
202 $ttl = $windowSeconds > 0 ? $windowSeconds : MINUTE_IN_SECONDS;
203 set_transient($transient_key, $attempts + 1, $ttl);
204 }
205
206 /**
207 * Validate login input
208 */
209 private function validateLoginInput(string $username, string $password): array
210 {
211 if (empty($username)) {
212 return [
213 'valid' => false,
214 'code' => 'empty_username',
215 'message' => __('Please enter your username or email.', 'yatra')
216 ];
217 }
218
219 if (empty($password)) {
220 return [
221 'valid' => false,
222 'code' => 'empty_password',
223 'message' => __('Please enter your password.', 'yatra')
224 ];
225 }
226
227 if (strlen($username) > 60) {
228 return [
229 'valid' => false,
230 'code' => 'invalid_username_length',
231 'message' => __('Username is too long.', 'yatra')
232 ];
233 }
234
235 if (strlen($password) > 72) {
236 return [
237 'valid' => false,
238 'code' => 'invalid_password_length',
239 'message' => __('Password is too long.', 'yatra')
240 ];
241 }
242
243 return ['valid' => true];
244 }
245
246 /**
247 * Authenticate user with enhanced security
248 */
249 private function authenticateUser(string $username, string $password)
250 {
251 // Use WordPress authentication with additional security
252 $user = wp_authenticate($username, $password);
253
254 if (is_wp_error($user)) {
255 return $user;
256 }
257
258 // Check if user is verified (if email verification is required)
259 if ($this->isEmailVerificationRequired() && !get_user_meta($user->ID, 'yatra_email_verified', true)) {
260 return new WP_Error('email_not_verified',
261 __('Your email address has not been verified. Please check your email.', 'yatra')
262 );
263 }
264
265 return $user;
266 }
267
268 /**
269 * Handle authentication errors
270 */
271 private function handleAuthenticationError(WP_Error $error): void
272 {
273 $error_code = $error->get_error_code();
274 $error_message = $error->get_error_message();
275
276 // Log security events
277 $this->logSecurityEvent('authentication_failed', [
278 'error_code' => $error_code,
279 'error_message' => $error_message
280 ]);
281
282 // Provide user-friendly error messages
283 if (in_array($error_code, ['invalid_username', 'incorrect_password'], true)) {
284 $error_message = __('Invalid username or password. Please try again.', 'yatra');
285 } elseif ($error_code === 'email_not_verified') {
286 // Keep the specific message from the error
287 } else {
288 $error_message = __('Login failed. Please try again.', 'yatra');
289 }
290
291 wp_send_json_error([
292 'success' => false,
293 'code' => $error_code,
294 'message' => $error_message
295 ]);
296 }
297
298 /**
299 * Setup secure user session
300 */
301 private function setupUserSession(\WP_User $user, bool $remember): void
302 {
303 wp_set_current_user($user->ID);
304 wp_set_auth_cookie($user->ID, $remember);
305
306 // Update user metadata
307 update_user_meta($user->ID, 'yatra_last_login', current_time('mysql'));
308 update_user_meta($user->ID, 'yatra_last_login_ip', $this->getClientIp());
309
310 // Clear failed login attempts
311 $transient_key = 'yatra_login_limit_' . md5($this->getClientIp());
312 delete_transient($transient_key);
313 }
314
315 /**
316 * Prepare secure success response
317 */
318 private function prepareSuccessResponse(\WP_User $user, string $redirect_to): array
319 {
320 // Apply filter for custom redirect logic
321 $redirect_url = apply_filters('yatra_login_redirect_url', $redirect_to, $user);
322
323 return [
324 'success' => true,
325 'message' => __('Login successful! Redirecting...', 'yatra'),
326 'user_id' => $user->ID,
327 'user_login' => $user->user_login,
328 'display_name' => $user->display_name,
329 'redirect_url' => esc_url($redirect_url),
330 'timestamp' => time()
331 ];
332 }
333
334 /**
335 * Validate redirect URL for security
336 */
337 private function validateRedirectUrl(string $redirect_url): string
338 {
339 if (empty($redirect_url)) {
340 return home_url('/' . \Yatra\Services\SettingsService::getAccountBase());
341 }
342
343 $redirect_url = sanitize_url($redirect_url);
344
345 // Validate URL is safe
346 if (!wp_http_validate_url($redirect_url)) {
347 return home_url('/' . \Yatra\Services\SettingsService::getAccountBase());
348 }
349
350 // Only allow redirects to same host
351 $redirect_host = parse_url($redirect_url, PHP_URL_HOST);
352 $site_host = parse_url(home_url(), PHP_URL_HOST);
353
354 if ($redirect_host !== $site_host) {
355 return home_url('/' . \Yatra\Services\SettingsService::getAccountBase());
356 }
357
358 return $redirect_url;
359 }
360
361 /**
362 * Check if user is allowed to login
363 */
364 private function isUserAllowedToLogin(\WP_User $user): bool
365 {
366 // Check if user is active
367 if (in_array('inactive', (array) $user->roles, true)) {
368 return false;
369 }
370
371 // Apply additional checks via filter
372 return apply_filters('yatra_user_allowed_to_login', true, $user);
373 }
374
375 /**
376 * Check if email verification is required
377 */
378 private function isEmailVerificationRequired(): bool
379 {
380 return apply_filters('yatra_require_email_verification', false);
381 }
382
383 /**
384 * Get client IP address
385 */
386 private function getClientIp(): string
387 {
388 $ip_keys = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP', 'REMOTE_ADDR'];
389
390 foreach ($ip_keys as $key) {
391 if (!empty($_SERVER[$key])) {
392 $ips = explode(',', $_SERVER[$key]);
393 $ip = trim($ips[0]);
394 if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
395 return $ip;
396 }
397 }
398 }
399
400 return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
401 }
402
403 /**
404 * Log security events
405 */
406 private function logSecurityEvent(string $event, array $data = []): void
407 {
408 if (!defined('WP_DEBUG') || !WP_DEBUG) {
409 return;
410 }
411
412 $log_data = array_merge([
413 'event' => $event,
414 'timestamp' => current_time('mysql'),
415 'ip' => $this->getClientIp(),
416 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? ''
417 ], $data);
418
419 }
420
421 /**
422 * Log login attempts
423 */
424 private function logLoginAttempt(string $username): void
425 {
426 if (!defined('WP_DEBUG') || !WP_DEBUG) {
427 return;
428 }
429
430 }
431 }
432