isHandled(); if (!$isYatra) { return $classes; } $classes = array_values(array_filter($classes, static function ($c) { return $c !== 'error404' && $c !== 'error-404'; })); if (!in_array('yatra-page', $classes, true)) { $classes[] = 'yatra-page'; } foreach ($ctx->getBodyClasses() as $extra) { if (!in_array($extra, $classes, true)) { $classes[] = $extra; } } return $classes; } /** * Public detection helper — true when the current request belongs to a Yatra route. * Centralises the logic previously in shouldClear404ForYatraRouting() so it can be * reused by pre_handle_404 and body_class filters. */ public static function isYatraRequest(): bool { return self::shouldClear404ForYatraRouting(); } /** * `template_include` filter — choose between PHP template and FSE block template. * * Decision flow when a Yatra handler has queued a template: * 1. If the admin has customised the matching virtual block template in the * Site Editor (wp_template post with source = 'custom'), defer to FSE — * WP renders the customised block template, which embeds Yatra content * via the `yatra/page-content` server block. Their edits take effect. * 2. Otherwise return Yatra's PHP template. It's faster, cache-friendly, * and the path the plugin tested most. * * For non-Yatra requests, return $template unchanged so the theme/FSE keep control. */ public static function filterTemplateInclude(string $template): string { $ctx = PageContext::instance(); if (!$ctx->hasTemplate()) { return $template; } // If the admin has saved a Site-Editor customisation for this page // type, render via WordPress's block-template canvas with the saved // content. loadCustomisedCanvas() returns null if no customisation // exists (the common case) or returns template-canvas.php with the // canvas globals primed. $pageType = $ctx->getPageType(); if ($pageType !== '') { $canvas = FseTemplates::loadCustomisedCanvas($pageType); if ($canvas !== null) { return $canvas; } } $selected = $ctx->getTemplate(); return $selected !== null ? $selected : $template; } /** * Handle template_redirect: * 1. Clear residual is_404 (paged-home quirk; pre_handle_404 catches the rest). * 2. Run the Router — handlers configure $wp_query and queue a template. * 3. Fall through to WordPress's template-loader. Our template_include * filter at priority 99 swaps in the Yatra template if one was queued. * * No include + exit here — that was the source of the FSE breakage. */ public static function handleTemplateRedirect(): void { global $wp_query; if (!empty($wp_query->is_404) && self::shouldClear404ForYatraRouting()) { $wp_query->is_404 = false; status_header(200); } if (!empty($wp_query->is_404)) { return; } if (!self::$router) { self::$router = new Router(); } $handled = self::$router->route(); // Plain-permalink fallback: ?yatra_booking_confirmation=... still needs // an explicit dispatch because PlainPageMatcher doesn't know about it. if (!$handled) { $confirmationId = get_query_var('yatra_booking_confirmation') ?: ($_GET['yatra_booking_confirmation'] ?? ($_GET['reference'] ?? ($_GET['booking_id'] ?? ''))); if (!empty($confirmationId)) { $handler = new BookingConfirmationPageHandler(); $handler->handle([ 'confirmation_id' => sanitize_text_field((string) $confirmationId), ]); } } // No exit. The selected Yatra template (if any) is in PageContext; // filterTemplateInclude() will return it from the template_include filter. } /** * True when this request should be routed by Yatra even if WP marked it 404 (paged home quirk). */ private static function shouldClear404ForYatraRouting(): bool { if (is_admin() || wp_doing_ajax() || wp_doing_cron()) { return false; } $yatraPage = isset($_GET['yatra_page']) ? trim((string) wp_unslash($_GET['yatra_page'])) : ''; if ($yatraPage !== '') { return true; } $qv = (string) get_query_var('yatra_page'); if ($qv !== '') { return true; } // Plain trip / taxonomy slug keys (same idea as {@see PermalinkCanonical::requestHasPlainYatraRoutingQuery}) $tripKey = preg_replace('/[^a-zA-Z0-9_-]/', '', (string) SettingsService::getTripBase()) ?: 'trip'; if (isset($_GET[$tripKey]) && is_string($_GET[$tripKey]) && trim(wp_unslash($_GET[$tripKey])) !== '') { return true; } foreach ( [ SettingsService::getDestinationBase(), SettingsService::getActivityBase(), SettingsService::getTripCategoryBase(), ] as $base ) { $bk = preg_replace('/[^a-zA-Z0-9_-]/', '', $base) ?: ''; if ($bk === '' || $bk === $tripKey) { continue; } if (isset($_GET[$bk]) && is_string($_GET[$bk]) && trim(wp_unslash($_GET[$bk])) !== '') { return true; } } foreach (['yatra_trip', 'yatra_trip_slug', 'yatra_destination_slug', 'yatra_activity_slug', 'yatra_category_slug', 'yatra_booking_confirmation', 'yatra_verify_email'] as $key) { if (!isset($_GET[$key])) { continue; } $v = wp_unslash($_GET[$key]); if (is_string($v) && trim($v) !== '') { return true; } } if ((string) get_query_var('yatra_verify_email') !== '') { return true; } $verifyPath = trim(UrlParser::getCleanRequestPath(), '/'); $verifyPrefix = SettingsService::getPermalinkBases()['email_verification_prefix']; if ($verifyPath !== '' && strpos($verifyPath, $verifyPrefix . '/') === 0) { return true; } return (bool) apply_filters('yatra_clear_404_for_routing', false); } /** * Add rewrite rules for trip permalinks and listing pages */ public static function addTripRewriteRules(): void { $bases = SettingsService::getPermalinkBases(); $trip_base = $bases['trip_base']; $booking_base = $bases['booking_base']; $account_base = $bases['account_base']; $destination_base = $bases['destination_base']; $activity_base = $bases['activity_base']; $trip_category_base = $bases['trip_category_base']; $bookingConfirmSeg = $bases['booking_flow_confirmation_segment']; $legacyBookingConfirmation = $bases['legacy_booking_confirmation_prefix']; $remainingCheckout = $bases['remaining_checkout_prefix']; $emailVerifyPrefix = $bases['email_verification_prefix']; // Add query vars first (must be registered before rewrite rules) // Single-trip slug query var matches trip URL base (e.g. trip=, tours=) add_rewrite_tag('%' . $trip_base . '%', '([^&]+)'); add_rewrite_tag('%yatra_booking_confirmation%', '([^&]+)'); add_rewrite_tag('%yatra_remaining_checkout%', '([^&]+)'); add_rewrite_tag('%yatra_verify_email%', '([^&]+)'); // Single taxonomy page tags add_rewrite_tag('%yatra_destination_slug%', '([^&]+)'); add_rewrite_tag('%yatra_activity_slug%', '([^&]+)'); add_rewrite_tag('%yatra_category_slug%', '([^&]+)'); add_rewrite_tag('%yatra_page%', '([a-zA-Z0-9_-]+)'); add_rewrite_tag('%paged%', '([0-9]+)'); // Add rewrite rule for email verification: /{email_verification_prefix}/{token}/ add_rewrite_rule( '^' . $emailVerifyPrefix . '/([a-zA-Z0-9_-]+)/?$', 'index.php?yatra_verify_email=$matches[1]', 'top' ); // Trip listing pagination: {trip_base}/page/{n}/ add_rewrite_rule( '^' . $trip_base . '/page/([0-9]+)/?$', 'index.php?yatra_page=' . $trip_base . '&paged=$matches[1]', 'top' ); // Trip listing (page 1): {trip_base}/ add_rewrite_rule( '^' . $trip_base . '/?$', 'index.php?yatra_page=' . $trip_base, 'top' ); // Customer account (pageless): /{account_base}/ and /{account_base}/{tab}/ // Must be real rewrite rules so WordPress doesn't 404 before Yatra Router runs. add_rewrite_rule( '^' . $account_base . '/?$', 'index.php?yatra_page=' . $account_base . '&yatra_account_page=dashboard', 'top' ); add_rewrite_rule( '^' . $account_base . '/([^/]+)/?$', 'index.php?yatra_page=' . $account_base . '&yatra_account_page=$matches[1]', 'top' ); // Single trip: {trip_base}/{trip_slug}/ add_rewrite_rule( '^' . $trip_base . '/([^/]+)/?$', 'index.php?yatra_page=' . $trip_base . '&' . $trip_base . '=$matches[1]', 'top' ); // Taxonomy: single + pagination (yatra_page = base from settings; paged = WP pagination) add_rewrite_rule( '^' . $destination_base . '/([^/]+)/page/([0-9]+)/?$', 'index.php?yatra_page=' . $destination_base . '&yatra_destination_slug=$matches[1]&paged=$matches[2]', 'top' ); add_rewrite_rule( '^' . $destination_base . '/([^/]+)/?$', 'index.php?yatra_page=' . $destination_base . '&yatra_destination_slug=$matches[1]', 'top' ); add_rewrite_rule( '^' . $destination_base . '/?$', 'index.php?yatra_page=' . $destination_base, 'top' ); add_rewrite_rule( '^' . $activity_base . '/([^/]+)/page/([0-9]+)/?$', 'index.php?yatra_page=' . $activity_base . '&yatra_activity_slug=$matches[1]&paged=$matches[2]', 'top' ); add_rewrite_rule( '^' . $activity_base . '/([^/]+)/?$', 'index.php?yatra_page=' . $activity_base . '&yatra_activity_slug=$matches[1]', 'top' ); add_rewrite_rule( '^' . $activity_base . '/?$', 'index.php?yatra_page=' . $activity_base, 'top' ); add_rewrite_rule( '^' . $trip_category_base . '/([^/]+)/page/([0-9]+)/?$', 'index.php?yatra_page=' . $trip_category_base . '&yatra_category_slug=$matches[1]&paged=$matches[2]', 'top' ); add_rewrite_rule( '^' . $trip_category_base . '/([^/]+)/?$', 'index.php?yatra_page=' . $trip_category_base . '&yatra_category_slug=$matches[1]', 'top' ); add_rewrite_rule( '^' . $trip_category_base . '/?$', 'index.php?yatra_page=' . $trip_category_base, 'top' ); // Pageless booking confirmation: /{booking_base}/{confirmation_segment}/{reference}/ (before trip slug rule) add_rewrite_rule( '^' . $booking_base . '/' . $bookingConfirmSeg . '/([a-zA-Z0-9_-]+)/?$', 'index.php?yatra_booking_confirmation=$matches[1]', 'top' ); // Booking with trip slug: /{booking_base}/{trip}/ add_rewrite_rule( '^' . $booking_base . '/([^/]+)/?$', 'index.php?yatra_page=' . $booking_base . '&trip=$matches[1]', 'top' ); // Booking hub (Settings → booking base, e.g. /book/) add_rewrite_rule( '^' . $booking_base . '/?$', 'index.php?yatra_page=' . $booking_base, 'top' ); // Legacy booking confirmation: /{legacy_booking_confirmation_prefix}/{reference} add_rewrite_rule( '^' . $legacyBookingConfirmation . '/([a-zA-Z0-9_-]+)/?$', 'index.php?yatra_booking_confirmation=$matches[1]', 'top' ); // Remaining checkout: /{remaining_checkout_prefix}/{token}/ add_rewrite_rule( '^' . $remainingCheckout . '/([a-zA-Z0-9_-]+)/?$', 'index.php?yatra_remaining_checkout=$matches[1]', 'top' ); /** * Fires after Yatra registers its core rewrite tags/rules. * * Use {@see \Yatra\Services\SettingsService::getPermalinkBases()} for the same slugs/helpers use. * * @param array $bases */ do_action('yatra_register_rewrite_rules', $bases); // Check if rewrite rules need flushing (only flush once after plugin update/activation) $rewrite_version = get_option('yatra_rewrite_rules_version', '0'); $current_version = '1.0.9'; // Increment this when rewrite rules change if ($rewrite_version !== $current_version) { flush_rewrite_rules(false); update_option('yatra_rewrite_rules_version', $current_version); } } /** * Add custom query vars for Yatra functionality */ public static function addCustomQueryVars(array $vars): array { $yatra_vars = [ 'yatra_trip', // legacy plain/pretty query var for trip slug 'yatra_booking_confirmation', 'yatra_remaining_checkout', 'yatra_verify_email', 'yatra_account_page', 'yatra_destination_slug', 'yatra_activity_slug', 'yatra_category_slug', 'yatra_page', 'paged', ]; // Add dynamic base names for plain permalink support $pb = SettingsService::getPermalinkBases(); $yatra_vars[] = $pb['trip_base']; $yatra_vars[] = $pb['destination_base']; $yatra_vars[] = $pb['activity_base']; $yatra_vars[] = $pb['trip_category_base']; return array_merge($vars, $yatra_vars); } /** * Get client IP address */ private static function getClientIp(): string { $ip_keys = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP', 'REMOTE_ADDR']; foreach ($ip_keys as $key) { if (!empty($_SERVER[$key])) { $ips = explode(',', $_SERVER[$key]); $ip = trim($ips[0]); if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { return $ip; } } } return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; } }