| @@ -293,8 +293,21 @@ | ||
| 293 | 293 | 'message' => __('New customer registration is disabled.', 'yatra'), |
| 294 | 294 | ], 403); |
| 295 | 295 | } |
| 296 | 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 | + | |
| 297 | 310 | $first_name = sanitize_text_field($request->get_param('first_name') ?? ''); |
| 298 | 311 | $last_name = sanitize_text_field($request->get_param('last_name') ?? ''); |
| 299 | 312 | $email = sanitize_email($request->get_param('email') ?? ''); |
| 300 | 313 | $phone = sanitize_text_field($request->get_param('phone') ?? ''); |
| @@ -483,14 +496,29 @@ | ||
| 483 | 496 | * Handle email verification |
| 484 | 497 | */ |
| 485 | 498 | public static function handleEmailVerification(): void |
| 486 | 499 | { |
| 487 | - $secure_token = get_query_var('yatra_verify_email'); | |
| 488 | - | |
| 489 | - if (empty($secure_token)) { | |
| 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 | + | |
| 490 | 509 | return; |
| 491 | 510 | } |
| 492 | 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 | + | |
| 493 | 521 | // Decode token |
| 494 | 522 | $secure_token = str_replace(['-', '_'], ['+', '/'], $secure_token); |
| 495 | 523 | $decoded = base64_decode($secure_token); |
| 496 | 524 | |
| @@ -514,67 +542,183 @@ | ||
| 514 | 542 | } |
| 515 | 543 | |
| 516 | 544 | $stored_token = get_user_meta($user_id, 'yatra_verification_token', true); |
| 517 | 545 | $token_expiry = get_user_meta($user_id, 'yatra_verification_token_expiry', true); |
| 518 | - | |
| 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 | + | |
| 519 | 559 | if (empty($stored_token) || $stored_token !== $token) { |
| 520 | 560 | self::showVerificationError(__('Invalid or expired verification link.', 'yatra')); |
| 521 | 561 | return; |
| 522 | 562 | } |
| 523 | - | |
| 563 | + | |
| 524 | 564 | if ($token_expiry && time() > (int) $token_expiry) { |
| 525 | 565 | self::showVerificationError(__('This verification link has expired. Please register again.', 'yatra')); |
| 526 | 566 | return; |
| 527 | 567 | } |
| 528 | - | |
| 568 | + | |
| 529 | 569 | // Mark as verified |
| 530 | 570 | update_user_meta($user_id, 'yatra_email_verified', '1'); |
| 531 | 571 | delete_user_meta($user_id, 'yatra_verification_token'); |
| 532 | 572 | delete_user_meta($user_id, 'yatra_verification_token_expiry'); |
| 533 | - | |
| 534 | - $redirect_url = add_query_arg(['email_verified' => '1'], yatra_get_checkout_url()); | |
| 535 | - wp_safe_redirect($redirect_url); | |
| 536 | - exit; | |
| 573 | + | |
| 574 | + self::showVerificationSuccess(false); | |
| 575 | + return; | |
| 537 | 576 | } |
| 538 | 577 | |
| 539 | 578 | /** |
| 540 | 579 | * Send verification email |
| 541 | 580 | */ |
| 542 | - private static function sendVerificationEmail(int $user_id, string $email, string $first_name, string $secure_token, bool $isResend = false): void | |
| 581 | + private static function sendVerificationEmail(int $_user_id, string $email, string $first_name, string $secure_token, bool $isResend = false): void | |
| 543 | 582 | { |
| 544 | - $verification_url = home_url('/yatra-verify-email/' . $secure_token . '/'); | |
| 545 | - $site_name = get_bloginfo('name'); | |
| 546 | - $subject = sprintf(__('[%s] Please verify your email address', 'yatra'), $site_name); | |
| 547 | - | |
| 548 | - $intro = $isResend | |
| 549 | - ? __("You requested a new verification link for your account at %s.", 'yatra') | |
| 550 | - : __("Thank you for registering at %s.", 'yatra'); | |
| 551 | - | |
| 552 | - $message = sprintf( | |
| 553 | - __("Hello %s,\n\n" . $intro . "\n\nPlease click the link below to verify your email address:\n\n%s\n\nThis link will expire in 24 hours.\n\nIf you did not " . ($isResend ? "request this" : "create this account") . ", please ignore this email.\n\nBest regards,\n%s", 'yatra'), | |
| 554 | - $first_name, | |
| 555 | - $site_name, | |
| 556 | - $verification_url, | |
| 557 | - $site_name | |
| 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 | + ) | |
| 558 | 610 | ); |
| 559 | 611 | |
| 560 | - $headers = ['Content-Type: text/plain; charset=UTF-8']; | |
| 561 | - wp_mail($email, $subject, $message, $headers); | |
| 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 | + ); | |
| 562 | 625 | } |
| 563 | 626 | |
| 564 | 627 | /** |
| 565 | - * Show verification error page | |
| 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. | |
| 566 | 631 | */ |
| 567 | 632 | private static function showVerificationError(string $message): void |
| 568 | 633 | { |
| 634 | + $heading = esc_html__('Verification Failed', 'yatra'); | |
| 635 | + $cta = esc_html__('Go to Homepage', 'yatra'); | |
| 636 | + | |
| 569 | 637 | wp_die( |
| 570 | 638 | '<div style="text-align: center; padding: 50px; font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, sans-serif;"> |
| 571 | - <h1 style="color: #dc2626; margin-bottom: 20px;">Verification Failed</h1> | |
| 639 | + <h1 style="color: #dc2626; margin-bottom: 20px;">' . $heading . '</h1> | |
| 572 | 640 | <p style="color: #4b5563; font-size: 16px; margin-bottom: 30px;">' . esc_html($message) . '</p> |
| 573 | - <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;">Go to Homepage</a> | |
| 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> | |
| 574 | 642 | </div>', |
| 575 | 643 | __('Verification Failed', 'yatra'), |
| 576 | 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] | |
| 577 | 721 | ); |
| 578 | 722 | } |
| 579 | 723 | } |
| 580 | 724 | |