| 1 |
<?php |
| 2 |
|
| 3 |
declare(strict_types=1); |
| 4 |
|
| 5 |
namespace Yatra\Controllers; |
| 6 |
|
| 7 |
/** |
| 8 |
* Authentication Controller |
| 9 |
* Handles user registration, login, email verification for Yatra customers |
| 10 |
* |
| 11 |
* @package Yatra |
| 12 |
*/ |
| 13 |
class AuthController |
| 14 |
{ |
| 15 |
/** |
| 16 |
* Register REST API routes |
| 17 |
*/ |
| 18 |
public static function registerRoutes(): void |
| 19 |
{ |
| 20 |
register_rest_route('yatra/v1', '/auth/login', [ |
| 21 |
'methods' => 'POST', |
| 22 |
'callback' => [self::class, 'login'], |
| 23 |
'permission_callback' => '__return_true', |
| 24 |
]); |
| 25 |
|
| 26 |
register_rest_route('yatra/v1', '/auth/register', [ |
| 27 |
'methods' => 'POST', |
| 28 |
'callback' => [self::class, 'register'], |
| 29 |
'permission_callback' => '__return_true', |
| 30 |
]); |
| 31 |
|
| 32 |
register_rest_route('yatra/v1', '/auth/resend-verification', [ |
| 33 |
'methods' => 'POST', |
| 34 |
'callback' => [self::class, 'resendVerification'], |
| 35 |
'permission_callback' => '__return_true', |
| 36 |
]); |
| 37 |
} |
| 38 |
|
| 39 |
/** |
| 40 |
* Register Yatra Customer role |
| 41 |
*/ |
| 42 |
public static function registerCustomerRole(): void |
| 43 |
{ |
| 44 |
if (!get_role('yatra_customer')) { |
| 45 |
add_role( |
| 46 |
'yatra_customer', |
| 47 |
__('Yatra Customer', 'yatra'), |
| 48 |
[ |
| 49 |
'read' => true, |
| 50 |
'edit_posts' => false, |
| 51 |
'delete_posts' => false, |
| 52 |
] |
| 53 |
); |
| 54 |
} |
| 55 |
} |
| 56 |
|
| 57 |
/** |
| 58 |
* Block unverified users from WordPress login |
| 59 |
* |
| 60 |
* @param \WP_User|\WP_Error|null $user |
| 61 |
* @param string $username |
| 62 |
* @param string $password |
| 63 |
* @return \WP_User|\WP_Error|null |
| 64 |
*/ |
| 65 |
public static function blockUnverifiedUserLogin($user, string $username, string $password) |
| 66 |
{ |
| 67 |
if (is_wp_error($user)) { |
| 68 |
return $user; |
| 69 |
} |
| 70 |
|
| 71 |
if (!($user instanceof \WP_User)) { |
| 72 |
return $user; |
| 73 |
} |
| 74 |
|
| 75 |
$has_verification_token = get_user_meta($user->ID, 'yatra_verification_token', true); |
| 76 |
$email_verified = get_user_meta($user->ID, 'yatra_email_verified', true); |
| 77 |
|
| 78 |
$needs_verification = !empty($has_verification_token); |
| 79 |
|
| 80 |
if (!$needs_verification) { |
| 81 |
$meta_exists = metadata_exists('user', $user->ID, 'yatra_email_verified'); |
| 82 |
$needs_verification = $meta_exists && !$email_verified; |
| 83 |
} |
| 84 |
|
| 85 |
if ($needs_verification) { |
| 86 |
$resend_url = add_query_arg([ |
| 87 |
'action' => 'yatra_resend_verification', |
| 88 |
'email' => urlencode($user->user_email), |
| 89 |
'_wpnonce' => wp_create_nonce('yatra_resend_verification'), |
| 90 |
], wp_login_url()); |
| 91 |
|
| 92 |
$message = sprintf( |
| 93 |
/* translators: %s: resend verification link */ |
| 94 |
__('<strong>Error:</strong> Please verify your email address before logging in. Check your inbox for the verification link. <br><br><a href="%s">Click here to resend verification email</a>', 'yatra'), |
| 95 |
esc_url($resend_url) |
| 96 |
); |
| 97 |
|
| 98 |
return new \WP_Error('email_not_verified', $message); |
| 99 |
} |
| 100 |
|
| 101 |
return $user; |
| 102 |
} |
| 103 |
|
| 104 |
/** |
| 105 |
* Handle resend verification from WordPress login page |
| 106 |
*/ |
| 107 |
public static function handleWpLoginResendVerification(): void |
| 108 |
{ |
| 109 |
if (!isset($_GET['action']) || $_GET['action'] !== 'yatra_resend_verification') { |
| 110 |
return; |
| 111 |
} |
| 112 |
|
| 113 |
// Verify nonce |
| 114 |
if (!isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_text_field($_GET['_wpnonce']), 'yatra_resend_verification')) { |
| 115 |
wp_die(__('Your session has expired. Please go back and try again.', 'yatra')); |
| 116 |
} |
| 117 |
|
| 118 |
$email = isset($_GET['email']) ? sanitize_email(urldecode($_GET['email'])) : ''; |
| 119 |
|
| 120 |
if (empty($email) || !is_email($email)) { |
| 121 |
wp_safe_redirect(add_query_arg('login_error', 'invalid_email', wp_login_url())); |
| 122 |
exit; |
| 123 |
} |
| 124 |
|
| 125 |
$user = get_user_by('email', $email); |
| 126 |
|
| 127 |
if (!$user) { |
| 128 |
// Redirect with generic success message (don't reveal if email exists) |
| 129 |
wp_safe_redirect(add_query_arg('checkemail', 'resent', wp_login_url())); |
| 130 |
exit; |
| 131 |
} |
| 132 |
|
| 133 |
// Check if already verified |
| 134 |
$email_verified = get_user_meta($user->ID, 'yatra_email_verified', true); |
| 135 |
if ($email_verified === '1') { |
| 136 |
wp_safe_redirect(add_query_arg('login_error', 'already_verified', wp_login_url())); |
| 137 |
exit; |
| 138 |
} |
| 139 |
|
| 140 |
// Check rate limiting |
| 141 |
$last_sent = get_user_meta($user->ID, 'yatra_verification_last_sent', true); |
| 142 |
if ($last_sent && (time() - (int) $last_sent) < 120) { |
| 143 |
$remaining = 120 - (time() - (int) $last_sent); |
| 144 |
wp_safe_redirect(add_query_arg([ |
| 145 |
'login_error' => 'rate_limited', |
| 146 |
'wait' => $remaining, |
| 147 |
], wp_login_url())); |
| 148 |
exit; |
| 149 |
} |
| 150 |
|
| 151 |
// Generate new token and send email |
| 152 |
$verification_token = wp_generate_password(32, false); |
| 153 |
$secure_token = base64_encode($user->ID . '|' . $verification_token . '|' . time()); |
| 154 |
$secure_token = str_replace(['+', '/', '='], ['-', '_', ''], $secure_token); |
| 155 |
|
| 156 |
update_user_meta($user->ID, 'yatra_verification_token', $verification_token); |
| 157 |
update_user_meta($user->ID, 'yatra_verification_token_expiry', time() + (24 * 60 * 60)); |
| 158 |
update_user_meta($user->ID, 'yatra_verification_last_sent', time()); |
| 159 |
|
| 160 |
// Send email |
| 161 |
$first_name = get_user_meta($user->ID, 'first_name', true) ?: $user->display_name; |
| 162 |
self::sendVerificationEmail($user->ID, $email, $first_name, $secure_token, true); |
| 163 |
|
| 164 |
wp_safe_redirect(add_query_arg('checkemail', 'resent', wp_login_url())); |
| 165 |
exit; |
| 166 |
} |
| 167 |
|
| 168 |
/** |
| 169 |
* Add custom messages to WordPress login page |
| 170 |
*/ |
| 171 |
public static function customLoginMessages(string $message): string |
| 172 |
{ |
| 173 |
if (isset($_GET['checkemail']) && $_GET['checkemail'] === 'resent') { |
| 174 |
$message = '<p class="message">' . __('A new verification link has been sent to your email address. Please check your inbox and spam folder.', 'yatra') . '</p>'; |
| 175 |
} |
| 176 |
|
| 177 |
if (isset($_GET['email_verified']) && $_GET['email_verified'] === '1') { |
| 178 |
$message = '<p class="message">' . __('Your email has been verified successfully! You can now log in.', 'yatra') . '</p>'; |
| 179 |
} |
| 180 |
|
| 181 |
return $message; |
| 182 |
} |
| 183 |
|
| 184 |
/** |
| 185 |
* Add custom error messages to WordPress login page |
| 186 |
*/ |
| 187 |
public static function customLoginErrors(\WP_Error $errors): \WP_Error |
| 188 |
{ |
| 189 |
if (isset($_GET['login_error'])) { |
| 190 |
switch ($_GET['login_error']) { |
| 191 |
case 'invalid_email': |
| 192 |
$errors->add('invalid_email', __('<strong>Error:</strong> Invalid email address.', 'yatra')); |
| 193 |
break; |
| 194 |
case 'already_verified': |
| 195 |
$errors->add('already_verified', __('<strong>Notice:</strong> Your email is already verified. Please log in.', 'yatra')); |
| 196 |
break; |
| 197 |
case 'rate_limited': |
| 198 |
$wait = isset($_GET['wait']) ? (int) $_GET['wait'] : 120; |
| 199 |
$errors->add('rate_limited', sprintf( |
| 200 |
/* translators: %d: number of seconds to wait */ |
| 201 |
__('<strong>Error:</strong> Please wait %d seconds before requesting another verification email.', 'yatra'), |
| 202 |
$wait |
| 203 |
)); |
| 204 |
break; |
| 205 |
} |
| 206 |
} |
| 207 |
|
| 208 |
return $errors; |
| 209 |
} |
| 210 |
|
| 211 |
/** |
| 212 |
* Handle login request |
| 213 |
*/ |
| 214 |
public static function login(\WP_REST_Request $request): \WP_REST_Response |
| 215 |
{ |
| 216 |
$username = sanitize_user($request->get_param('username') ?? ''); |
| 217 |
$password = $request->get_param('password') ?? ''; |
| 218 |
$remember = !empty($request->get_param('remember')); |
| 219 |
|
| 220 |
if (empty($username) || empty($password)) { |
| 221 |
return new \WP_REST_Response([ |
| 222 |
'success' => false, |
| 223 |
'message' => __('Please enter your username/email and password.', 'yatra'), |
| 224 |
], 400); |
| 225 |
} |
| 226 |
|
| 227 |
// Get user to check verification status |
| 228 |
$user_check = get_user_by('login', $username); |
| 229 |
if (!$user_check) { |
| 230 |
$user_check = get_user_by('email', $username); |
| 231 |
} |
| 232 |
|
| 233 |
// Check email verification for Yatra users |
| 234 |
if ($user_check) { |
| 235 |
$has_verification_token = get_user_meta($user_check->ID, 'yatra_verification_token', true); |
| 236 |
$email_verified = get_user_meta($user_check->ID, 'yatra_email_verified', true); |
| 237 |
|
| 238 |
if (!empty($has_verification_token)) { |
| 239 |
return new \WP_REST_Response([ |
| 240 |
'success' => false, |
| 241 |
'message' => __('Please verify your email address before logging in. Check your inbox for the verification link.', 'yatra'), |
| 242 |
'needs_verification' => true, |
| 243 |
'email' => $user_check->user_email, |
| 244 |
], 403); |
| 245 |
} |
| 246 |
|
| 247 |
$meta_exists = metadata_exists('user', $user_check->ID, 'yatra_email_verified'); |
| 248 |
if ($meta_exists && !$email_verified) { |
| 249 |
return new \WP_REST_Response([ |
| 250 |
'success' => false, |
| 251 |
'message' => __('Please verify your email address before logging in. Check your inbox for the verification link.', 'yatra'), |
| 252 |
'needs_verification' => true, |
| 253 |
'email' => $user_check->user_email, |
| 254 |
], 403); |
| 255 |
} |
| 256 |
} |
| 257 |
|
| 258 |
// Authenticate |
| 259 |
$user = wp_signon([ |
| 260 |
'user_login' => $username, |
| 261 |
'user_password' => $password, |
| 262 |
'remember' => $remember, |
| 263 |
], is_ssl()); |
| 264 |
|
| 265 |
if (is_wp_error($user)) { |
| 266 |
$error_message = wp_strip_all_tags($user->get_error_message()); |
| 267 |
if (strpos($error_message, 'incorrect') !== false || strpos($error_message, 'Invalid') !== false) { |
| 268 |
$error_message = __('Invalid username/email or password. Please try again.', 'yatra'); |
| 269 |
} |
| 270 |
return new \WP_REST_Response([ |
| 271 |
'success' => false, |
| 272 |
'message' => $error_message, |
| 273 |
], 401); |
| 274 |
} |
| 275 |
|
| 276 |
wp_set_current_user($user->ID); |
| 277 |
|
| 278 |
return new \WP_REST_Response([ |
| 279 |
'success' => true, |
| 280 |
'message' => __('Login successful! Redirecting...', 'yatra'), |
| 281 |
'user_id' => $user->ID, |
| 282 |
]); |
| 283 |
} |
| 284 |
|
| 285 |
/** |
| 286 |
* Handle registration request |
| 287 |
*/ |
| 288 |
public static function register(\WP_REST_Request $request): \WP_REST_Response |
| 289 |
{ |
| 290 |
if (!\Yatra\Services\SettingsService::isEnabled('customer_registration')) { |
| 291 |
return new \WP_REST_Response([ |
| 292 |
'success' => false, |
| 293 |
'message' => __('New customer registration is disabled.', 'yatra'), |
| 294 |
], 403); |
| 295 |
} |
| 296 |
|
| 297 |
// reCAPTCHA v3 (no-op unless the registration form is protected in settings). |
| 298 |
$recaptcha = \Yatra\Services\RecaptchaService::verifyForm( |
| 299 |
'registration', |
| 300 |
(string) ($request->get_param('recaptcha_token') ?? ''), |
| 301 |
$_SERVER['REMOTE_ADDR'] ?? null |
| 302 |
); |
| 303 |
if (empty($recaptcha['success'])) { |
| 304 |
return new \WP_REST_Response([ |
| 305 |
'success' => false, |
| 306 |
'message' => $recaptcha['message'] ?? __('reCAPTCHA verification failed.', 'yatra'), |
| 307 |
], 400); |
| 308 |
} |
| 309 |
|
| 310 |
$first_name = sanitize_text_field($request->get_param('first_name') ?? ''); |
| 311 |
$last_name = sanitize_text_field($request->get_param('last_name') ?? ''); |
| 312 |
$email = sanitize_email($request->get_param('email') ?? ''); |
| 313 |
$phone = sanitize_text_field($request->get_param('phone') ?? ''); |
| 314 |
$password = $request->get_param('password') ?? ''; |
| 315 |
$confirm_password = $request->get_param('confirm_password') ?? ''; |
| 316 |
|
| 317 |
// Validation |
| 318 |
if (empty($first_name)) { |
| 319 |
return new \WP_REST_Response([ |
| 320 |
'success' => false, |
| 321 |
'message' => __('Please enter your first name.', 'yatra'), |
| 322 |
], 400); |
| 323 |
} |
| 324 |
|
| 325 |
if (empty($last_name)) { |
| 326 |
return new \WP_REST_Response([ |
| 327 |
'success' => false, |
| 328 |
'message' => __('Please enter your last name.', 'yatra'), |
| 329 |
], 400); |
| 330 |
} |
| 331 |
|
| 332 |
if (empty($email) || !is_email($email)) { |
| 333 |
return new \WP_REST_Response([ |
| 334 |
'success' => false, |
| 335 |
'message' => __('Please enter a valid email address.', 'yatra'), |
| 336 |
], 400); |
| 337 |
} |
| 338 |
|
| 339 |
if (empty($password) || strlen($password) < 8) { |
| 340 |
return new \WP_REST_Response([ |
| 341 |
'success' => false, |
| 342 |
'message' => __('Password must be at least 8 characters long.', 'yatra'), |
| 343 |
], 400); |
| 344 |
} |
| 345 |
|
| 346 |
if ($password !== $confirm_password) { |
| 347 |
return new \WP_REST_Response([ |
| 348 |
'success' => false, |
| 349 |
'message' => __('Passwords do not match.', 'yatra'), |
| 350 |
], 400); |
| 351 |
} |
| 352 |
|
| 353 |
if (email_exists($email)) { |
| 354 |
return new \WP_REST_Response([ |
| 355 |
'success' => false, |
| 356 |
'message' => __('An account with this email already exists. Please login instead.', 'yatra'), |
| 357 |
], 409); |
| 358 |
} |
| 359 |
|
| 360 |
// Generate username |
| 361 |
$username = sanitize_user(current(explode('@', $email))); |
| 362 |
$original_username = $username; |
| 363 |
$counter = 1; |
| 364 |
|
| 365 |
while (username_exists($username)) { |
| 366 |
$username = $original_username . $counter; |
| 367 |
$counter++; |
| 368 |
} |
| 369 |
|
| 370 |
// Create user |
| 371 |
$user_id = wp_create_user($username, $password, $email); |
| 372 |
|
| 373 |
if (is_wp_error($user_id)) { |
| 374 |
$error_message = wp_strip_all_tags($user_id->get_error_message()); |
| 375 |
return new \WP_REST_Response([ |
| 376 |
'success' => false, |
| 377 |
'message' => $error_message, |
| 378 |
], 500); |
| 379 |
} |
| 380 |
|
| 381 |
// Assign Yatra Customer role |
| 382 |
$user = new \WP_User($user_id); |
| 383 |
$user->set_role('yatra_customer'); |
| 384 |
|
| 385 |
// Update user meta |
| 386 |
wp_update_user([ |
| 387 |
'ID' => $user_id, |
| 388 |
'first_name' => $first_name, |
| 389 |
'last_name' => $last_name, |
| 390 |
'display_name' => $first_name . ' ' . $last_name, |
| 391 |
]); |
| 392 |
|
| 393 |
if (!empty($phone)) { |
| 394 |
update_user_meta($user_id, 'billing_phone', $phone); |
| 395 |
update_user_meta($user_id, 'phone', $phone); |
| 396 |
} |
| 397 |
|
| 398 |
// Email verification |
| 399 |
update_user_meta($user_id, 'yatra_email_verified', '0'); |
| 400 |
|
| 401 |
$verification_token = wp_generate_password(32, false); |
| 402 |
$secure_token = base64_encode($user_id . '|' . $verification_token . '|' . time()); |
| 403 |
$secure_token = str_replace(['+', '/', '='], ['-', '_', ''], $secure_token); |
| 404 |
|
| 405 |
update_user_meta($user_id, 'yatra_verification_token', $verification_token); |
| 406 |
update_user_meta($user_id, 'yatra_verification_token_expiry', time() + (24 * 60 * 60)); |
| 407 |
|
| 408 |
// Send verification email |
| 409 |
self::sendVerificationEmail($user_id, $email, $first_name, $secure_token); |
| 410 |
|
| 411 |
// Admin notification |
| 412 |
wp_new_user_notification($user_id, null, 'admin'); |
| 413 |
|
| 414 |
return new \WP_REST_Response([ |
| 415 |
'success' => true, |
| 416 |
'message' => __('Registration successful! Please check your email to verify your account before logging in.', 'yatra'), |
| 417 |
'user_id' => $user_id, |
| 418 |
'require_verification' => true, |
| 419 |
]); |
| 420 |
} |
| 421 |
|
| 422 |
/** |
| 423 |
* Handle resend verification request |
| 424 |
*/ |
| 425 |
public static function resendVerification(\WP_REST_Request $request): \WP_REST_Response |
| 426 |
{ |
| 427 |
$email = sanitize_email($request->get_param('email') ?? ''); |
| 428 |
|
| 429 |
if (empty($email) || !is_email($email)) { |
| 430 |
return new \WP_REST_Response([ |
| 431 |
'success' => false, |
| 432 |
'message' => __('Please enter a valid email address.', 'yatra'), |
| 433 |
], 400); |
| 434 |
} |
| 435 |
|
| 436 |
$user = get_user_by('email', $email); |
| 437 |
|
| 438 |
if (!$user) { |
| 439 |
return new \WP_REST_Response([ |
| 440 |
'success' => true, |
| 441 |
'message' => __('If an account with this email exists and is pending verification, a new verification link has been sent.', 'yatra'), |
| 442 |
]); |
| 443 |
} |
| 444 |
|
| 445 |
$email_verified = get_user_meta($user->ID, 'yatra_email_verified', true); |
| 446 |
|
| 447 |
if ($email_verified === '1' || $email_verified === 1 || $email_verified === true) { |
| 448 |
return new \WP_REST_Response([ |
| 449 |
'success' => false, |
| 450 |
'message' => __('This email is already verified. You can login now.', 'yatra'), |
| 451 |
], 400); |
| 452 |
} |
| 453 |
|
| 454 |
$meta_exists = metadata_exists('user', $user->ID, 'yatra_email_verified'); |
| 455 |
$has_verification_token = get_user_meta($user->ID, 'yatra_verification_token', true); |
| 456 |
|
| 457 |
if (!$meta_exists && empty($has_verification_token)) { |
| 458 |
return new \WP_REST_Response([ |
| 459 |
'success' => true, |
| 460 |
'message' => __('If an account with this email exists and is pending verification, a new verification link has been sent.', 'yatra'), |
| 461 |
]); |
| 462 |
} |
| 463 |
|
| 464 |
// Rate limiting |
| 465 |
$last_sent = get_user_meta($user->ID, 'yatra_verification_last_sent', true); |
| 466 |
if ($last_sent && (time() - (int) $last_sent) < 120) { |
| 467 |
$remaining = 120 - (time() - (int) $last_sent); |
| 468 |
return new \WP_REST_Response([ |
| 469 |
'success' => false, |
| 470 |
'message' => __('Please wait before requesting another verification email.', 'yatra'), |
| 471 |
'rate_limited' => true, |
| 472 |
'remaining_seconds' => $remaining, |
| 473 |
], 429); |
| 474 |
} |
| 475 |
|
| 476 |
// Generate new token |
| 477 |
$verification_token = wp_generate_password(32, false); |
| 478 |
$secure_token = base64_encode($user->ID . '|' . $verification_token . '|' . time()); |
| 479 |
$secure_token = str_replace(['+', '/', '='], ['-', '_', ''], $secure_token); |
| 480 |
|
| 481 |
update_user_meta($user->ID, 'yatra_verification_token', $verification_token); |
| 482 |
update_user_meta($user->ID, 'yatra_verification_token_expiry', time() + (24 * 60 * 60)); |
| 483 |
update_user_meta($user->ID, 'yatra_verification_last_sent', time()); |
| 484 |
|
| 485 |
// Send email |
| 486 |
$first_name = get_user_meta($user->ID, 'first_name', true) ?: $user->display_name; |
| 487 |
self::sendVerificationEmail($user->ID, $email, $first_name, $secure_token, true); |
| 488 |
|
| 489 |
return new \WP_REST_Response([ |
| 490 |
'success' => true, |
| 491 |
'message' => __('A new verification link has been sent to your email address. Please check your inbox.', 'yatra'), |
| 492 |
]); |
| 493 |
} |
| 494 |
|
| 495 |
/** |
| 496 |
* Handle email verification |
| 497 |
*/ |
| 498 |
public static function handleEmailVerification(): void |
| 499 |
{ |
| 500 |
$secure_token = (string) get_query_var('yatra_verify_email'); |
| 501 |
if ($secure_token === '' && isset($_GET['yatra_verify_email'])) { |
| 502 |
$raw = wp_unslash($_GET['yatra_verify_email']); |
| 503 |
$secure_token = is_string($raw) ? preg_replace('/[^a-zA-Z0-9_-]/', '', $raw) ?? '' : ''; |
| 504 |
} |
| 505 |
|
| 506 |
if ($secure_token === '') { |
| 507 |
self::showVerificationError(__('Invalid verification link.', 'yatra')); |
| 508 |
|
| 509 |
return; |
| 510 |
} |
| 511 |
|
| 512 |
$previewToken = defined('YATRA_EMAIL_VERIFICATION_PREVIEW_TOKEN') |
| 513 |
? (string) YATRA_EMAIL_VERIFICATION_PREVIEW_TOKEN |
| 514 |
: 'preview-verify-token'; |
| 515 |
if ($secure_token === $previewToken) { |
| 516 |
self::showEmailVerificationPreviewNotice(); |
| 517 |
|
| 518 |
return; |
| 519 |
} |
| 520 |
|
| 521 |
// Decode token |
| 522 |
$secure_token = str_replace(['-', '_'], ['+', '/'], $secure_token); |
| 523 |
$decoded = base64_decode($secure_token); |
| 524 |
|
| 525 |
if (!$decoded || strpos($decoded, '|') === false) { |
| 526 |
self::showVerificationError(__('Invalid verification link.', 'yatra')); |
| 527 |
return; |
| 528 |
} |
| 529 |
|
| 530 |
$parts = explode('|', $decoded); |
| 531 |
if (count($parts) < 2) { |
| 532 |
self::showVerificationError(__('Invalid verification link.', 'yatra')); |
| 533 |
return; |
| 534 |
} |
| 535 |
|
| 536 |
$user_id = (int) $parts[0]; |
| 537 |
$token = $parts[1]; |
| 538 |
|
| 539 |
if ($user_id <= 0 || empty($token)) { |
| 540 |
self::showVerificationError(__('Invalid verification link.', 'yatra')); |
| 541 |
return; |
| 542 |
} |
| 543 |
|
| 544 |
$stored_token = get_user_meta($user_id, 'yatra_verification_token', true); |
| 545 |
$token_expiry = get_user_meta($user_id, 'yatra_verification_token_expiry', true); |
| 546 |
|
| 547 |
// Idempotent path: the token+expiry are deleted as soon as a successful |
| 548 |
// verification completes (see below), so a second click on the same |
| 549 |
// link previously fell into the "Invalid or expired" branch and made |
| 550 |
// already-verified customers believe their account was broken. Detect |
| 551 |
// the prior-success state explicitly and reuse the success page so the |
| 552 |
// outcome is clear regardless of how many times the link is clicked. |
| 553 |
$already_verified = get_user_meta($user_id, 'yatra_email_verified', true) === '1'; |
| 554 |
if ($already_verified) { |
| 555 |
self::showVerificationSuccess(true); |
| 556 |
return; |
| 557 |
} |
| 558 |
|
| 559 |
if (empty($stored_token) || $stored_token !== $token) { |
| 560 |
self::showVerificationError(__('Invalid or expired verification link.', 'yatra')); |
| 561 |
return; |
| 562 |
} |
| 563 |
|
| 564 |
if ($token_expiry && time() > (int) $token_expiry) { |
| 565 |
self::showVerificationError(__('This verification link has expired. Please register again.', 'yatra')); |
| 566 |
return; |
| 567 |
} |
| 568 |
|
| 569 |
// Mark as verified |
| 570 |
update_user_meta($user_id, 'yatra_email_verified', '1'); |
| 571 |
delete_user_meta($user_id, 'yatra_verification_token'); |
| 572 |
delete_user_meta($user_id, 'yatra_verification_token_expiry'); |
| 573 |
|
| 574 |
self::showVerificationSuccess(false); |
| 575 |
return; |
| 576 |
} |
| 577 |
|
| 578 |
/** |
| 579 |
* Send verification email |
| 580 |
*/ |
| 581 |
private static function sendVerificationEmail(int $_user_id, string $email, string $first_name, string $secure_token, bool $isResend = false): void |
| 582 |
{ |
| 583 |
$verificationUrl = function_exists('yatra_get_email_verification_url') |
| 584 |
? yatra_get_email_verification_url($secure_token) |
| 585 |
: home_url('/yatra-verify-email/' . rawurlencode($secure_token) . '/'); |
| 586 |
|
| 587 |
$siteName = get_bloginfo('name'); |
| 588 |
$introParagraph = $isResend |
| 589 |
? sprintf( |
| 590 |
/* translators: %s: site name */ |
| 591 |
__('You requested a new verification link for your account at %s. Click the button below to verify your email address.', 'yatra'), |
| 592 |
$siteName |
| 593 |
) |
| 594 |
: sprintf( |
| 595 |
/* translators: %s: site name */ |
| 596 |
__('Thank you for registering at %s. Please verify your email address to activate your account.', 'yatra'), |
| 597 |
$siteName |
| 598 |
); |
| 599 |
|
| 600 |
$footerNote = $isResend |
| 601 |
? __('If you did not request this email, you can ignore it.', 'yatra') |
| 602 |
: __('If you did not create this account, you can ignore this email.', 'yatra'); |
| 603 |
|
| 604 |
$expiryNoticeHtml = esc_html( |
| 605 |
sprintf( |
| 606 |
/* translators: %d: hours until link expiry */ |
| 607 |
__('This verification link expires in %d hours for your security.', 'yatra'), |
| 608 |
24 |
| 609 |
) |
| 610 |
); |
| 611 |
|
| 612 |
\Yatra\Services\TransactionalEmailTemplateService::sendIfEnabled( |
| 613 |
\Yatra\Services\TransactionalEmailTemplateService::TYPE_CUSTOMER_EMAIL_VERIFICATION, |
| 614 |
$email, |
| 615 |
[ |
| 616 |
'customer_first_name' => $first_name, |
| 617 |
'customer_name' => $first_name, |
| 618 |
'customer_email' => $email, |
| 619 |
'verification_link' => $verificationUrl, |
| 620 |
'intro_paragraph' => $introParagraph, |
| 621 |
'footer_note' => $footerNote, |
| 622 |
'expiry_notice_html' => $expiryNoticeHtml, |
| 623 |
] |
| 624 |
); |
| 625 |
} |
| 626 |
|
| 627 |
/** |
| 628 |
* Show verification error page (hard-fail dead-end with a route back home). |
| 629 |
* Every string is translatable — operators run Yatra in many locales and |
| 630 |
* the pre-3.0.5 hardcoded "Verification Failed" heading was untranslatable. |
| 631 |
*/ |
| 632 |
private static function showVerificationError(string $message): void |
| 633 |
{ |
| 634 |
$heading = esc_html__('Verification Failed', 'yatra'); |
| 635 |
$cta = esc_html__('Go to Homepage', 'yatra'); |
| 636 |
|
| 637 |
wp_die( |
| 638 |
'<div style="text-align: center; padding: 50px; font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, sans-serif;"> |
| 639 |
<h1 style="color: #dc2626; margin-bottom: 20px;">' . $heading . '</h1> |
| 640 |
<p style="color: #4b5563; font-size: 16px; margin-bottom: 30px;">' . esc_html($message) . '</p> |
| 641 |
<a href="' . esc_url(home_url()) . '" style="display: inline-block; background: #3b82f6; color: #fff; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">' . $cta . '</a> |
| 642 |
</div>', |
| 643 |
__('Verification Failed', 'yatra'), |
| 644 |
['response' => 400] |
| 645 |
); |
| 646 |
} |
| 647 |
|
| 648 |
/** |
| 649 |
* Render an unambiguous "email verified" confirmation page. |
| 650 |
* |
| 651 |
* The pre-3.0.5 flow silently 302-redirected to the checkout URL with a |
| 652 |
* `?email_verified=1` flag, relying on `booking-auth.php` to surface a |
| 653 |
* one-liner notice. That notice only renders when the auth form itself |
| 654 |
* renders (no booking session / logged-in user → notice never shown), so |
| 655 |
* customers regularly saw "nothing happened" after clicking the link. |
| 656 |
* |
| 657 |
* Now we render a dedicated success page with explicit confirmation, the |
| 658 |
* verified email-state, and a primary CTA back into the booking flow. |
| 659 |
* Idempotent: a second click on the same link reaches `$already=true` |
| 660 |
* and shows "Your email is already verified" instead of the misleading |
| 661 |
* "Invalid or expired link" error. |
| 662 |
* |
| 663 |
* @param bool $already True when the user has already been verified by an |
| 664 |
* earlier click on the same link (idempotent path). |
| 665 |
*/ |
| 666 |
private static function showVerificationSuccess(bool $already): void |
| 667 |
{ |
| 668 |
$checkoutUrl = function_exists('yatra_get_checkout_url') |
| 669 |
? yatra_get_checkout_url() |
| 670 |
: home_url('/'); |
| 671 |
$continueUrl = add_query_arg(['email_verified' => '1'], $checkoutUrl); |
| 672 |
|
| 673 |
$heading = $already |
| 674 |
? esc_html__('Email Already Verified', 'yatra') |
| 675 |
: esc_html__('Email Verified', 'yatra'); |
| 676 |
$message = $already |
| 677 |
? esc_html__('Your email address is already verified — no further action is needed. You can continue with your booking.', 'yatra') |
| 678 |
: esc_html__('Your email address has been verified successfully. You can now log in and continue with your booking.', 'yatra'); |
| 679 |
$cta = esc_html__('Continue to Checkout', 'yatra'); |
| 680 |
$homeCta = esc_html__('Go to Homepage', 'yatra'); |
| 681 |
$title = $already |
| 682 |
? __('Email Already Verified', 'yatra') |
| 683 |
: __('Email Verified', 'yatra'); |
| 684 |
|
| 685 |
// Inline-only styling so the page renders correctly regardless of |
| 686 |
// theme stylesheet load order (wp_die() can fire before themes |
| 687 |
// enqueue their styles). |
| 688 |
$body = '<div style="text-align: center; padding: 50px 20px; max-width: 520px; margin: 0 auto; font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, sans-serif;">' |
| 689 |
. '<div style="display: inline-flex; align-items: center; justify-content: center; width: 72px; height: 72px; border-radius: 50%; background: #d1fae5; margin: 0 auto 24px;">' |
| 690 |
. '<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#059669" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' |
| 691 |
. '<polyline points="20 6 9 17 4 12"></polyline>' |
| 692 |
. '</svg>' |
| 693 |
. '</div>' |
| 694 |
. '<h1 style="color: #065f46; margin: 0 0 12px; font-size: 26px;">' . $heading . '</h1>' |
| 695 |
. '<p style="color: #4b5563; font-size: 16px; line-height: 1.6; margin: 0 0 28px;">' . $message . '</p>' |
| 696 |
. '<a href="' . esc_url($continueUrl) . '" style="display: inline-block; background: #059669; color: #fff; padding: 12px 28px; border-radius: 8px; text-decoration: none; font-weight: 600; margin-right: 8px;">' . $cta . '</a>' |
| 697 |
. '<a href="' . esc_url(home_url()) . '" style="display: inline-block; color: #4b5563; padding: 12px 16px; text-decoration: none; font-weight: 500;">' . $homeCta . '</a>' |
| 698 |
. '</div>'; |
| 699 |
|
| 700 |
wp_die($body, $title, ['response' => 200]); |
| 701 |
} |
| 702 |
|
| 703 |
/** |
| 704 |
* Inform users that the link is only for email template previews. |
| 705 |
*/ |
| 706 |
private static function showEmailVerificationPreviewNotice(): void |
| 707 |
{ |
| 708 |
$body = '<div style="text-align: center; padding: 50px; max-width: 520px; margin: 0 auto; font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, sans-serif;"> |
| 709 |
<h1 style="color: #1e40af; margin-bottom: 16px;">' . esc_html__('Sample verification link', 'yatra') . '</h1> |
| 710 |
<p style="color: #4b5563; font-size: 16px; line-height: 1.6; margin-bottom: 24px;">' . esc_html__( |
| 711 |
'This URL is used only in email previews and test messages. It does not verify an account. Use the link from your real verification email to activate your account.', |
| 712 |
'yatra' |
| 713 |
) . '</p> |
| 714 |
<a href="' . esc_url(home_url()) . '" style="display: inline-block; background: #3b82f6; color: #fff; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">' . esc_html__('Go to homepage', 'yatra') . '</a> |
| 715 |
</div>'; |
| 716 |
|
| 717 |
wp_die( |
| 718 |
$body, |
| 719 |
__('Email verification (preview)', 'yatra'), |
| 720 |
['response' => 200] |
| 721 |
); |
| 722 |
} |
| 723 |
} |
| 724 |
|
| 725 |
|