get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE trip_id = %d AND customer_id = %d AND status = 'completed'", $trip_id, $user_id )); if (!$has_booking) { return false; } } // Check if user already reviewed this trip (but allow if within edit window) if ($user_id > 0) { $existing_review = yatra_get_user_review($trip_id, $user_id); if ($existing_review && !yatra_can_edit_review($existing_review)) { return false; } } return true; } /** * Get user's existing review for a trip * * @param int $trip_id Trip ID * @param int|null $user_id User ID (defaults to current user) * @return object|null Review object or null */ function yatra_get_user_review(int $trip_id, ?int $user_id = null): ?object { if ($user_id === null) { $user_id = get_current_user_id(); } if ($user_id === 0) { return null; } global $wpdb; $table = ReviewsTable::getTableName(); $review = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$table} WHERE trip_id = %d AND user_id = %d ORDER BY created_at DESC LIMIT 1", $trip_id, $user_id )); return $review ?: null; } /** * Check if a review can be edited (within 24 hours of creation and not approved) * * @param object $review Review object with created_at and status fields * @return bool */ function yatra_can_edit_review(object $review): bool { if (empty($review->created_at)) { return false; } // Don't allow editing if review is approved if (isset($review->status) && $review->status === 'approved') { return false; } $created_time = strtotime($review->created_at); $current_time = current_time('timestamp'); $hours_since_creation = ($current_time - $created_time) / 3600; // Allow editing within 24 hours (only for pending/rejected reviews) return $hours_since_creation <= 24; } /** * Get time remaining to edit a review * * @param object $review Review object with created_at field * @return string Human-readable time remaining (e.g., "5 hours", "30 minutes") */ function yatra_get_review_edit_time_remaining(object $review): string { if (empty($review->created_at)) { return ''; } $created_time = strtotime($review->created_at); $current_time = current_time('timestamp'); $seconds_since_creation = $current_time - $created_time; $seconds_remaining = (24 * 3600) - $seconds_since_creation; if ($seconds_remaining <= 0) { return ''; } $hours = floor($seconds_remaining / 3600); $minutes = floor(($seconds_remaining % 3600) / 60); if ($hours > 0) { /* translators: %d: number of hours remaining. */ return sprintf(_n('%d hour', '%d hours', $hours, 'yatra'), $hours); } /* translators: %d: number of minutes remaining. */ return sprintf(_n('%d minute', '%d minutes', $minutes, 'yatra'), $minutes); } /** * Get the booking URL for a trip * * @param string $trip_slug The trip slug * @param array $params Optional URL parameters (date, adults, children, price) * @return string The booking URL */ function yatra_get_booking_url(string $trip_slug, array $params = []): string { $permalink_structure = get_option('permalink_structure'); $is_plain = empty($permalink_structure); // Check if using custom booking page via SettingsService if (SettingsService::useCustomBookingPage()) { $page_url = get_permalink(SettingsService::getBookingPageId()); if ($page_url) { $params['trip'] = $trip_slug; return add_query_arg($params, $page_url); } } // Using default dynamic URL $booking_base = SettingsService::getBookingBase(); if ($is_plain) { $params['trip'] = $trip_slug; return add_query_arg( array_merge(['yatra_page' => $booking_base], $params), home_url('/') ); } $url = home_url('/' . $booking_base . '/' . $trip_slug); if (!empty($params)) { $url = add_query_arg($params, $url); } return $url; } /** * Normalized Dynamic Pricing display toggles (listing, trip page, availability). * * @return array{show_original_price: bool, show_savings_badge: bool, show_urgency_messages: bool} */ if (!function_exists('yatra_get_dynamic_pricing_display_flags')) { function yatra_get_dynamic_pricing_display_flags(): array { $s = apply_filters('yatra_get_dynamic_pricing_display_settings', [ 'show_original_price' => true, 'show_savings_badge' => true, 'show_urgency_messages' => false, ]); return [ 'show_original_price' => filter_var($s['show_original_price'] ?? true, FILTER_VALIDATE_BOOLEAN), 'show_savings_badge' => filter_var($s['show_savings_badge'] ?? true, FILTER_VALIDATE_BOOLEAN), 'show_urgency_messages' => filter_var($s['show_urgency_messages'] ?? false, FILTER_VALIDATE_BOOLEAN), ]; } } /** * Urgency lines for a trip surface (listing card, sidebar, similar trips). Pro fills via yatra_trip_card_dynamic_pricing_meta. * * @param array $context base_sale_price, base_original_price, departure_date, spots_remaining, … * @return array */ if (!function_exists('yatra_trip_card_dynamic_pricing_urgency_lines')) { function yatra_trip_card_dynamic_pricing_urgency_lines(int $trip_id, array $context = []): array { if ($trip_id <= 0) { return []; } $flags = yatra_get_dynamic_pricing_display_flags(); if (!$flags['show_urgency_messages']) { return []; } $meta = apply_filters( 'yatra_trip_card_dynamic_pricing_meta', ['urgency_messages' => []], array_merge($context, [ 'trip_id' => $trip_id, 'display' => $flags, ]) ); if (!is_array($meta) || empty($meta['urgency_messages']) || !is_array($meta['urgency_messages'])) { return []; } $out = []; foreach ($meta['urgency_messages'] as $line) { $line = sanitize_text_field((string) $line); if ($line !== '') { $out[] = $line; } } return array_values(array_unique($out)); } } /** * Format price with currency * * @param float $amount The amount to format * @param string|null $currency The currency code (optional, uses global setting if not provided) * @param bool $zero_is_unknown When true (default), 0 is shown as "Contact for pricing" (trip/listing). * Set false for checkout, payments, and invoices where 0 is a real amount. * @return string Formatted price */ if (!function_exists('yatra_format_price')) { function yatra_format_price(float $amount, ?string $currency = null, bool $zero_is_unknown = true): string { if ($zero_is_unknown && (empty($amount) || $amount == 0)) { return __('Contact for pricing', 'yatra'); } // Get currency from global settings if not provided if (empty($currency)) { $currency = SettingsService::getCurrency(); } // Get formatting settings from global settings $currency_position = SettingsService::getCurrencyPosition(); // Single source of truth: honors the admin "Number of decimals" field and // stays in sync with the JS price formatter (already clamped to 0–4). $decimal_places = SettingsService::getPriceDecimals(); $thousand_separator = SettingsService::getString('thousand_separator', ','); $decimal_separator = SettingsService::getString('decimal_separator', '.'); // Format the amount with proper separators $formatted_amount = number_format($amount, $decimal_places, $decimal_separator, $thousand_separator); // Get currency symbol $currency_symbol = yatra_get_currency_symbol($currency); // Placement: Settings UI uses left, right, left_space, right_space; legacy uses before/after. $raw = strtolower(trim((string) $currency_position)); if ($raw === 'before') { $pos = 'left_space'; } elseif ($raw === 'after') { $pos = 'right_space'; } else { $pos = $raw; } $allowed = ['left', 'right', 'left_space', 'right_space']; if (!in_array($pos, $allowed, true)) { $pos = 'left_space'; } if ($pos === 'right') { return $formatted_amount . $currency_symbol; } if ($pos === 'right_space') { return $formatted_amount . ' ' . $currency_symbol; } if ($pos === 'left') { return $currency_symbol . $formatted_amount; } return $currency_symbol . ' ' . $formatted_amount; } } /** * Get currency symbol from currency code * * @param string $currency_code The currency code (e.g., 'USD', 'EUR', 'NPR') * @return string The currency symbol or code */ if (!function_exists('yatra_get_currency_symbol')) { function yatra_get_currency_symbol(string $currency_code): string { $symbols = [ 'USD' => '$', 'EUR' => '€', 'GBP' => '£', 'JPY' => '¥', 'CNY' => '¥', 'INR' => '₹', 'NPR' => 'Rs', 'AUD' => 'A$', 'CAD' => 'C$', 'CHF' => 'CHF', 'NZD' => 'NZ$', 'SGD' => 'S$', 'HKD' => 'HK$', 'KRW' => '₩', 'THB' => '฿', 'MYR' => 'RM', 'PHP' => '₱', 'IDR' => 'Rp', 'VND' => '₫', 'BRL' => 'R$', 'MXN' => 'MX$', 'RUB' => '₽', 'ZAR' => 'R', 'AED' => 'د.إ', 'SAR' => '﷼', 'TRY' => '₺', 'SEK' => 'kr', 'NOK' => 'kr', 'DKK' => 'kr', 'PLN' => 'zł', 'CZK' => 'Kč', 'HUF' => 'Ft', 'ILS' => '₪', 'TWD' => 'NT$', 'PKR' => '₨', 'BDT' => '৳', 'LKR' => 'Rs', 'EGP' => 'E£', 'NGN' => '₦', 'KES' => 'KSh', // Ghanaian cedi – use plain symbol without the GH prefix 'GHS' => '₵', 'GHC' => '₵', 'ARS' => 'AR$', 'CLP' => 'CL$', 'COP' => 'CO$', 'PEN' => 'S/', ]; return $symbols[strtoupper($currency_code)] ?? $currency_code; } } /** * Format duration (days/nights) * * @param int $days Number of days * @param int|null $nights Number of nights (optional) * @return string Formatted duration */ if (!function_exists('yatra_format_duration')) { function yatra_format_duration(int $days, ?int $nights = null, ?int $hours = null): string { // Hour-based (single-day) tours take precedence when a positive hours // value is supplied. Optional trailing arg keeps every existing // two-argument call unchanged. if ($hours !== null && $hours > 0) { return sprintf( /* translators: %d: number of hours. */ _n('%d hour', '%d hours', $hours, 'yatra'), $hours ); } if ($days > 0 && $nights !== null && $nights > 0) { /* translators: 1: number of days, 2: number of nights. */ return sprintf(__('%1$d days / %2$d nights', 'yatra'), $days, $nights); } if ($days > 0) { return sprintf( /* translators: %d: number of days. */ _n('%d day', '%d days', $days, 'yatra'), $days ); } return __('Flexible', 'yatra'); } } /** * Render SVG icon * * @param string $icon_name Icon name * @param string $class Optional CSS class * @return string SVG markup */ if (!function_exists('yatra_svg_icon')) { function yatra_svg_icon(string $icon_name, string $class = ''): string { static $icons = null; // Load icons from JSON file once if ($icons === null) { $icons_file = dirname(__FILE__) . '/icons.json'; if (file_exists($icons_file)) { $icons_data = json_decode(file_get_contents($icons_file), true); $icons = []; // Convert JSON data to PHP array foreach ($icons_data as $name => $data) { if (isset($data['svg'])) { $icons[$name] = (string) $data['svg']; } } } else { $icons = []; } } $svg = $icons[$icon_name] ?? ''; if ($svg === '' || !is_string($svg)) { return ''; } if ($class !== '') { $class_attr = esc_attr($class); if (preg_match('/]*\sclass="([^"]*)"/i', $svg, $m)) { $existing = trim((string) ($m[1] ?? '')); $merged = trim($existing . ' ' . $class_attr); $svg = preg_replace('/(]*\sclass=")([^"]*)(")/i', '$1' . $merged . '$3', $svg, 1); } else { $svg = preg_replace('/ 64) { return ''; } if (!preg_match('/^[a-z0-9-]+$/', $n)) { return ''; } return $n; } } /** * Normalize icon picker payload before storing (REST / services). * * @param array $icon * @return array{type: string, value: string|int, provider?: string} */ if (!function_exists('yatra_normalize_icon_picker_for_storage')) { function yatra_normalize_icon_picker_for_storage(array $icon): array { $type = isset($icon['type']) && $icon['type'] === 'image' ? 'image' : 'icon'; $value = $icon['value'] ?? ''; if ($type === 'image') { return [ 'type' => 'image', 'value' => is_numeric($value) ? (int) $value : sanitize_text_field((string) $value), ]; } $provider = isset($icon['provider']) ? sanitize_key((string) $icon['provider']) : 'yatra'; if (!in_array($provider, ['yatra', 'fa-solid', 'fa-regular'], true)) { $provider = 'yatra'; } return [ 'type' => 'icon', 'value' => sanitize_text_field((string) $value), 'provider' => $provider, ]; } } /** * Markup for a stored icon picker value (Yatra SVG registry, Font Awesome, or image). * * @param array|string|null $picker Serialized JSON string, array, or null. * @return string Safe HTML (empty string if nothing renderable). */ if (!function_exists('yatra_stored_picker_icon_markup')) { function yatra_stored_picker_icon_markup($picker, string $default_yatra_slug = 'mountain', string $class = ''): string { $class = trim($class); $class_attr = $class !== '' ? ' ' . esc_attr($class) : ''; if ($picker === null || $picker === '') { return function_exists('yatra_svg_icon') ? yatra_svg_icon($default_yatra_slug, $class) : ''; } if (is_string($picker) && strpos($picker, '{') === 0) { $picker = json_decode($picker, true); } if (!is_array($picker) || !isset($picker['type'])) { if (is_string($picker)) { $slug = trim($picker); return $slug !== '' && function_exists('yatra_svg_icon') ? yatra_svg_icon($slug, $class) : yatra_svg_icon($default_yatra_slug, $class); } return function_exists('yatra_svg_icon') ? yatra_svg_icon($default_yatra_slug, $class) : ''; } if ($picker['type'] === 'image' && !empty($picker['value'])) { $image_url = is_numeric($picker['value']) ? wp_get_attachment_url((int) $picker['value']) : (string) $picker['value']; if ($image_url) { $style = 'width:24px;height:24px;object-fit:cover;border-radius:4px;'; return ''; } return function_exists('yatra_svg_icon') ? yatra_svg_icon('image', $class) : ''; } if ($picker['type'] === 'icon' && !empty($picker['value'])) { $provider = isset($picker['provider']) ? sanitize_key((string) $picker['provider']) : 'yatra'; if ($provider === 'fa-solid' || $provider === 'fa-regular') { $slug = yatra_sanitize_fa_icon_slug((string) $picker['value']); if ($slug === '') { return function_exists('yatra_svg_icon') ? yatra_svg_icon($default_yatra_slug, $class) : ''; } $fa_prefix = $provider === 'fa-regular' ? 'fa-regular' : 'fa-solid'; return ''; } return function_exists('yatra_svg_icon') ? yatra_svg_icon((string) $picker['value'], $class) : ''; } return function_exists('yatra_svg_icon') ? yatra_svg_icon($default_yatra_slug, $class) : ''; } } /** * Translated display label for trip meal_plan stored slug (matches admin Trip Builder options). * * @param string|null $slug Raw value from DB (e.g. half_board, "Half Board"). */ if (!function_exists('yatra_meal_plan_label')) { function yatra_meal_plan_label(?string $slug): string { if ($slug === null || $slug === '') { return ''; } $s = strtolower(trim(preg_replace('/[\s\-]+/', '_', $slug), " \t\n\r\0\x0B_-")); switch ($s) { case 'breakfast': return __('Breakfast Only', 'yatra'); case 'half_board': return __('Half Board (Breakfast + Dinner)', 'yatra'); case 'full_board': return __('Full Board (All Meals)', 'yatra'); case 'all_inclusive': return __('All Inclusive', 'yatra'); case 'none': return __('No Meals Included', 'yatra'); default: return ucwords(str_replace('_', ' ', $s)); } } } /** * Translated itinerary entry item type label for frontend (matches admin item type names). */ if (!function_exists('yatra_itinerary_item_type_label')) { function yatra_itinerary_item_type_label(string $type): string { $t = trim($type); switch ($t) { case 'Meal': return __('Meal', 'yatra'); case 'Activity': return __('Activity', 'yatra'); case 'Accommodation': return __('Accommodation', 'yatra'); case 'Transportation': return __('Transportation', 'yatra'); case 'Rest': return __('Rest', 'yatra'); default: return $t; } } } /** * Extract SVG icon slug from a stored icon field (same shape as admin / archive cards). * * @param mixed $icon Raw value from DB (serialized array with type/value, URL, attachment id, or legacy slug string). */ function yatra_icon_slug_from_stored_field($icon): string { if ($icon === null || $icon === '') { return ''; } $icon = maybe_unserialize($icon); if (is_array($icon)) { $type = $icon['type'] ?? $icon[0] ?? ''; $value = $icon['value'] ?? $icon[1] ?? ''; if ($type === 'icon' && !empty($value) && is_string($value)) { $provider = isset($icon['provider']) ? sanitize_key((string) $icon['provider']) : 'yatra'; if ($provider === 'fa-solid' || $provider === 'fa-regular') { return ''; } return $value; } return ''; } if (is_string($icon)) { if (filter_var($icon, FILTER_VALIDATE_URL)) { return ''; } $slug = trim($icon); return $slug !== '' ? $slug : ''; } return ''; } /** * SVG markup for archive listing CTAs: use admin icon when present and valid in icons.json; else default slug. * * @param string $resolved_icon_slug From the listing loop (same source as card hero icon when type is "icon"). * @param string $default_slug icons.json key when no admin icon. */ function yatra_archive_listing_cta_icon_markup(string $resolved_icon_slug, string $default_slug, string $class = 'yatra-btn-icon'): string { $slug = trim($resolved_icon_slug); if ($slug !== '' && function_exists('yatra_svg_icon')) { $out = yatra_svg_icon($slug, $class); if ($out !== '') { return $out; } } $fallback = trim($default_slug); if ($fallback !== '' && function_exists('yatra_svg_icon')) { return yatra_svg_icon($fallback, $class); } return ''; } /** * Icon slug for trip listing card "View Details" — category, then destination, then difficulty (backend order). * * @param array> $categories Trip categories from getCategories() * @param array> $destinations Trip destinations from getDestinations() * @param array $difficulty From Trip::getDifficulty() */ function yatra_trip_listing_card_cta_icon_slug(array $categories, array $destinations, array $difficulty): string { foreach ($categories as $row) { if (empty($row)) { continue; } $raw = is_object($row) ? ($row->icon ?? null) : ($row['icon'] ?? null); $slug = yatra_icon_slug_from_stored_field($raw); if ($slug !== '') { return $slug; } } foreach ($destinations as $row) { if (empty($row)) { continue; } $raw = is_object($row) ? ($row->icon ?? null) : ($row['icon'] ?? null); $slug = yatra_icon_slug_from_stored_field($raw); if ($slug !== '') { return $slug; } } if (!empty($difficulty['icon']) && is_string($difficulty['icon'])) { $try = trim($difficulty['icon']); if ($try !== '') { return $try; } } return ''; } /** * Get booking base URL slug * * @return string The booking base slug */ function yatra_get_booking_base(): string { // Check if using custom booking page if (SettingsService::useCustomBookingPage()) { $booking_page_id = SettingsService::getBookingPageId(); if ($booking_page_id > 0) { $page = get_post($booking_page_id); if ($page) { return $page->post_name; } } } return SettingsService::getBookingBase(); } /** * Check if the current page is a booking page * * @return bool */ function yatra_is_booking_page(): bool { global $wp_query; // Check for custom booking page if (SettingsService::useCustomBookingPage()) { $booking_page_id = SettingsService::getBookingPageId(); if ($booking_page_id > 0 && is_page($booking_page_id)) { return true; } } $booking_base = SettingsService::getBookingBase(); if (!empty($wp_query->get('yatra_page')) && (string) $wp_query->get('yatra_page') === $booking_base) { return true; } // Check for dynamic booking URL return !empty($wp_query->get('yatra_booking_trip_slug')); } /** * Get the global trip object * * Similar to WordPress get_post(), this function returns the current trip object * when on a single trip page. * * @return object|null The trip object or null if not on a trip page */ function yatra_get_trip(): ?object { global $trip; return $trip ?? null; } /** * Check if we're on a single trip page * * @return bool True if on a single trip page */ function yatra_is_single_trip(): bool { global $wp_query; return !empty($wp_query->get('yatra_trip_id')); } /** * Get a trip field value with default fallback * * @param string $field The field name * @param mixed $default Default value if field is empty * @return mixed The field value or default */ function yatra_get_trip_field(string $field, $default = '') { global $trip; if (!$trip || !isset($trip->$field)) { return $default; } return $trip->$field ?: $default; } /** * Echo a trip field value with escaping * * @param string $field The field name * @param string $escape Escape function: 'html', 'attr', 'url', 'js', 'none' * @param mixed $default Default value if field is empty */ function yatra_trip_field(string $field, string $escape = 'html', $default = ''): void { $value = yatra_get_trip_field($field, $default); switch ($escape) { case 'html': echo esc_html($value); break; case 'attr': echo esc_attr($value); break; case 'url': echo esc_url($value); break; case 'js': echo esc_js($value); break; case 'none': case 'kses': echo wp_kses_post($value); break; default: echo esc_html($value); } } /** * ============================================ * BRAND / WHITE LABEL HELPERS (THIN WRAPPERS) * ============================================ * * The free plugin owns the function NAMES (so callers in plugin row meta, * admin menu, PDF templates, etc. work without conditional `function_exists` * checks), but every override lives in Yatra Pro's White Label module. * * Each helper here just applies a filter; Pro's WhiteLabel module registers * filter callbacks when the module is enabled AND an Agency-tier license is * active. Without Pro, every filter no-ops and these return the defaults — * which is the correct behavior for a free-only install. * * Option storage, REST endpoints, sanitization, plugin-list rebranding, * brand-color CSS injection, and dependency-link rewriting all live in * yatra-pro/app/Modules/WhiteLabel/ — NOT here. */ /** * Public URL for the Yatra brand icon (admin menu + React sidebar). * Defaults to the bundled `yatra-icon.png`; Pro overrides via the * `yatra_brand_icon_url` filter when a White Label logo is configured. */ function yatra_get_brand_icon_url(): string { $default = ''; if (defined('YATRA_PLUGIN_PATH') && defined('YATRA_PLUGIN_URL')) { $candidates = [ 'assets/images/yatra-icon.png', 'assets/images/yara-icon.png', ]; foreach ($candidates as $relative) { $file = YATRA_PLUGIN_PATH . $relative; if (!is_readable($file)) { continue; } $default = add_query_arg( 'ver', (string) filemtime($file), YATRA_PLUGIN_URL . $relative ); break; } } return (string) apply_filters('yatra_brand_icon_url', $default); } /** * Whether the Agency White Label module is active and may override branding. * Pro returns true via the `yatra_white_label_active` filter when its * WhiteLabel module is enabled AND the license tier is Agency. */ function yatra_is_white_label_active(): bool { return (bool) apply_filters('yatra_white_label_active', false); } /** * Read a single white-label setting with a default fallback. Backed by a * filter so option access stays in Pro. * * @param mixed $default * @return mixed */ function yatra_get_white_label_setting(string $key, $default = '') { return apply_filters('yatra_white_label_setting', $default, $key); } /** * @return array */ function yatra_get_white_label_settings(): array { $value = apply_filters('yatra_white_label_settings', []); return is_array($value) ? $value : []; } /** * Branding for generated PDFs (invoice, voucher, itinerary). * * Free ships an unbranded default — the header keeps whatever colour the * document already used and no logo is shown — so nothing changes for a site * without Yatra Pro. The White Label module hooks these filters to supply the * operator's own logo and colour, exactly as it already does for * `yatra_brand_icon_url` and friends. * * Kept as filters rather than reading White Label options directly so free never * depends on Pro, and so a site can brand its PDFs from a theme or snippet * without the module at all. * * @param string $defaultHeaderColor The document's existing header colour, so * each PDF keeps its own look when unbranded. * @return array{logo_url: string, header_color: string} */ function yatra_get_pdf_branding(string $defaultHeaderColor): array { $logo = (string) apply_filters('yatra_pdf_branding_logo_url', ''); $color = (string) apply_filters('yatra_pdf_branding_header_color', $defaultHeaderColor); // Only accept a well-formed hex colour; anything else falls back to the // document default rather than emitting broken CSS into the PDF. if (!preg_match('/^#[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3})?$/', $color)) { $color = $defaultHeaderColor; } $logo = esc_url_raw(trim($logo)); return [ 'logo_url' => $logo, 'header_color' => $color, ]; } /** * Are partial payments possible on this site at all? * * True when deposits or partial payments are switched on globally. Used to * decide whether part-payment specific features (such as the separate * "part payment received" email template) are relevant — there is no point * showing them to an operator who only ever takes payment in full. */ function yatra_partial_payments_enabled(): bool { $enabled = \Yatra\Services\SettingsService::isEnabled('partial_payment') || \Yatra\Services\SettingsService::isEnabled('enable_deposit') || \Yatra\Services\SettingsService::isEnabled('deposit_required'); return (bool) apply_filters('yatra_partial_payments_enabled', $enabled); } /** * How many stars to draw for an average rating. * * Rounds to the NEAREST half star rather than flooring. Flooring made a 4.9 * average draw four-and-a-half stars, which reads as a mistake sitting next to * the printed "4.9" — a 4.9 is five stars to anyone looking at it. * * 4.9 -> 5 4.7 -> 4.5 4.4 -> 4.5 4.2 -> 4 * * Returns the number of solid stars and whether a half star follows them, so * every surface (confirmation page, reviews block, listing cards) draws the * same rating identically. * * @return array{full:int, half:bool} */ function yatra_rating_star_parts($rating): array { $rating = max(0.0, min(5.0, (float) $rating)); // Work in half-star units so the rounding is a single, obvious step. $halves = (int) round($rating * 2); return [ 'full' => intdiv($halves, 2), 'half' => ($halves % 2) === 1, ]; } /** * Branded plugin name shown in admin menu, plugin list, and PDFs. */ function yatra_get_brand_name(): string { return (string) apply_filters('yatra_brand_name', 'Yatra'); } /** * Branded company/author name (replaces "MantraBrain"). */ function yatra_get_brand_company(): string { return (string) apply_filters('yatra_brand_company', 'MantraBrain'); } /** * Public website URL for the branded product. */ function yatra_get_brand_website_url(): string { $url = (string) apply_filters('yatra_brand_website_url', 'https://wpyatra.com/'); return $url !== '' ? esc_url_raw($url) : 'https://wpyatra.com/'; } /** * Support URL surfaced in admin notices and the plugin row. */ function yatra_get_brand_support_url(): string { $url = (string) apply_filters( 'yatra_brand_support_url', 'https://wordpress.org/support/plugin/yatra/reviews/?filter=5' ); return $url !== '' ? esc_url_raw($url) : 'https://wordpress.org/support/plugin/yatra/reviews/?filter=5'; } /** * ============================================ * BOOKING SESSION MANAGEMENT * ============================================ */ /** * Start WordPress session if not already started */ function yatra_start_session(): void { // Start output buffering to prevent accidental output from breaking sessions if (!ob_get_level()) { ob_start(); } if (session_status() === PHP_SESSION_NONE && !headers_sent()) { // Set session cookie parameters for better compatibility if (PHP_VERSION_ID >= 70300) { session_set_cookie_params([ 'lifetime' => 0, 'path' => defined('COOKIEPATH') ? COOKIEPATH : '/', 'domain' => defined('COOKIE_DOMAIN') ? COOKIE_DOMAIN : '', 'secure' => is_ssl(), 'httponly' => true, 'samesite' => 'Lax' ]); } session_start(); } } /** * Set booking session data * * @param array $data Booking data to store */ function yatra_set_booking_session(array $data): void { yatra_start_session(); // Clear any existing remaining payment session to avoid conflicts unset($_SESSION['yatra_remaining']); $session_data = array_merge( $_SESSION['yatra_booking'] ?? [], $data, ['timestamp' => time()] ); $_SESSION['yatra_booking'] = $session_data; // ALWAYS store in transient as backup (not just for REST API) // This ensures data persists across all request types // Generate or reuse booking token $booking_token = $_SESSION['yatra_booking_token'] ?? 'yatra_booking_' . wp_generate_password(32, false); $_SESSION['yatra_booking_token'] = $booking_token; $session_data['booking_token'] = $booking_token; // Store in transient (expires in 30 minutes) try { $transient_set = set_transient($booking_token, $session_data, 1800); } catch (Exception $e) { // Continue without transient - session fallback will be used } } /** * Get booking session data * * @param string|null $key Specific key to retrieve, or null for all data * @param mixed $default Default value if key not found * @return mixed */ function yatra_get_booking_session(?string $key = null, $default = null) { yatra_start_session(); $booking_data = $_SESSION['yatra_booking'] ?? []; // If session is empty, try to restore from transient (REST API → page load transition) if (empty($booking_data) || empty($booking_data['trip_id'])) { // Check for booking token in URL or session $booking_token = $_GET['booking_token'] ?? $_SESSION['yatra_booking_token'] ?? null; if ($booking_token) { try { $transient_data = get_transient($booking_token); if ($transient_data && is_array($transient_data) && !empty($transient_data['trip_id'])) { // Validate transient data integrity if (isset($transient_data['timestamp']) && (time() - $transient_data['timestamp']) < 1800) { $booking_data = $transient_data; // Restore to session $_SESSION['yatra_booking'] = $booking_data; $_SESSION['yatra_booking_token'] = $booking_token; } } } catch (Exception $e) { // Continue without transient data } } } // Check if session is expired (30 minutes) if (!empty($booking_data['timestamp'])) { $session_age = time() - $booking_data['timestamp']; if ($session_age > 1800) { // 30 minutes yatra_clear_booking_session(); return $key ? $default : []; } } if ($key === null) { return $booking_data; } return $booking_data[$key] ?? $default; } /** * Clear booking session data (PHP session, booking token, and REST backup transient). * * Without removing the token and transient, yatra_get_booking_session() can repopulate * checkout data from the transient on the next request after a completed booking. */ function yatra_clear_booking_session(): void { yatra_start_session(); $token = $_SESSION['yatra_booking_token'] ?? null; if (is_string($token) && $token !== '') { delete_transient($token); } unset($_SESSION['yatra_booking'], $_SESSION['yatra_booking_token']); } /** * Check if booking session exists and is valid * * @return bool */ function yatra_has_booking_session(): bool { $booking_data = yatra_get_booking_session(); return !empty($booking_data) && !empty($booking_data['trip_id']); } /** * Fire {@see 'yatra_booking_confirmed'} when a booking reaches `confirmed` from a non-confirmed status. * * Core always fired `yatra_booking_status_changed`; Pro modules (Trip Consent, Google Calendar) listen * on this dedicated action. Call this after any code path that sets a booking to `confirmed` without * going through {@see \Yatra\Services\BookingService::updateStatus()}. * * @param int $bookingId Booking ID. * @param string $previousStatus Booking status in the database immediately before confirming. */ function yatra_trigger_booking_confirmed(int $bookingId, string $previousStatus): void { if ($bookingId < 1 || $previousStatus === 'confirmed') { return; } $repo = new \Yatra\Repositories\BookingRepository(); $booking = $repo->findWithTrip($bookingId); if (!$booking || ($booking->status ?? '') !== 'confirmed') { return; } /** * Booking reached confirmed status (was not confirmed before this transition). * * @param int $bookingId Booking ID. * @param object $booking Row from {@see \Yatra\Repositories\BookingRepository::findWithTrip()}. */ do_action('yatra_booking_confirmed', $bookingId, $booking); } /** * Decide whether a successful payment should auto-confirm the booking. * * A booking auto-confirms on payment only when the operator has enabled * "Auto-Confirm Bookings", OR the booking is now fully paid. A deposit / * partial payment must NOT confirm the booking while auto-confirm is off — the * operator confirms it manually. Previously the synchronous-gateway, Stripe and * PayPal completion paths force-confirmed on any payment, so deposit bookings * were confirmed immediately regardless of the setting. * * @param bool $fullyPaid Whether the booking's balance is now zero. * @param int $bookingId Booking ID (passed to the filter for context). * @return bool True to set the booking to `confirmed`. */ function yatra_should_confirm_booking_on_payment(bool $fullyPaid, int $bookingId = 0): bool { $autoConfirm = (bool) \Yatra\Services\SettingsService::isEnabled('auto_confirm_bookings'); $shouldConfirm = $autoConfirm || $fullyPaid; /** * Filter whether a completed payment auto-confirms the booking. * * @param bool $shouldConfirm Default: auto-confirm setting is on OR fully paid. * @param bool $fullyPaid Whether the balance is now zero. * @param int $bookingId Booking ID. * @param bool $autoConfirm The `auto_confirm_bookings` setting value. */ return (bool) apply_filters('yatra_confirm_booking_on_payment', $shouldConfirm, $fullyPaid, $bookingId, $autoConfirm); } /** * ============================================ * REMAINING PAYMENT SESSION MANAGEMENT * ============================================ */ /** * Set remaining payment session data * * @param array $data Remaining payment data to store */ function yatra_set_remaining_session(array $data): void { yatra_start_session(); // Clear checkout session fully (including token + transient) before remaining-payment flow yatra_clear_booking_session(); $_SESSION['yatra_remaining'] = array_merge( $data, ['timestamp' => time()] ); // Ensure session data is written to storage immediately if (session_status() === PHP_SESSION_ACTIVE) { session_write_close(); } } /** * Get remaining payment session data * * @param string|null $key Specific key to retrieve, or null for all data * @param mixed $default Default value if key not found * @return mixed */ function yatra_get_remaining_session(?string $key = null, $default = null) { yatra_start_session(); $remaining_data = $_SESSION['yatra_remaining'] ?? []; // Check if session is expired (30 minutes) if (!empty($remaining_data['timestamp'])) { $session_age = time() - $remaining_data['timestamp']; if ($session_age > 1800) { // 30 minutes yatra_clear_remaining_session(); return $key ? $default : []; } } if ($key === null) { return $remaining_data; } return $remaining_data[$key] ?? $default; } /** * Clear remaining payment session data */ function yatra_clear_remaining_session(): void { yatra_start_session(); unset($_SESSION['yatra_remaining']); } /** * Check if remaining payment session exists and is valid * * @return bool */ function yatra_has_remaining_session(): bool { $remaining_data = yatra_get_remaining_session(); return !empty($remaining_data) && !empty($remaining_data['booking_id']); } /** * Get the active checkout session type * * @return string|null 'remaining' if remaining session exists, 'booking' if booking session exists, null if neither */ function yatra_get_checkout_session_type(): ?string { if (yatra_has_remaining_session()) { return 'remaining'; } if (yatra_has_booking_session()) { return 'booking'; } return null; } /** * Get the active checkout session data (remaining or booking) * * @return array Session data with 'type' key indicating session type */ function yatra_get_active_checkout_session(): array { if (yatra_has_remaining_session()) { $data = yatra_get_remaining_session(); $data['session_type'] = 'remaining'; return $data; } if (yatra_has_booking_session()) { $data = yatra_get_booking_session(); $data['session_type'] = 'booking'; return $data; } return []; } /** * Get booking/checkout URL * * Logic: * 1. If custom booking page is set → return that page's URL * 2. Otherwise → return dynamic URL using booking_base from settings (e.g., /bookings/) * * @return string Booking URL */ function yatra_get_checkout_url(): string { $permalink_structure = get_option('permalink_structure'); $is_plain = empty($permalink_structure); // Check if custom booking page is set via SettingsService if (SettingsService::useCustomBookingPage()) { $page_id = SettingsService::getBookingPageId(); if ($page_id > 0) { return get_permalink($page_id); } } // Default dynamic URL using booking base from settings $base = SettingsService::getBookingBase(); if ($is_plain) { return add_query_arg(['yatra_page' => $base], home_url('/')); } return home_url('/' . $base . '/'); } /** * Front-end URL for booking confirmation for a given reference. * * Booking confirmation is pageless: Yatra serves it via rewrite rules and query vars, * not a WordPress page permalink. Pretty URLs use /{booking_base}/confirmation/{reference}/. * Plain permalinks use ?yatra_booking_confirmation={reference}. * * To use a real WordPress page as the base (advanced), filter {@see 'yatra_booking_confirmation_base_url'}. * Legacy /booking-confirmation/{reference}/ remains registered in rewrites for old links. * * @param string $reference Booking reference segment (may be empty for base URL only). * @return string Full URL. */ function yatra_get_booking_confirmation_url(string $reference = ''): string { $reference = (string) $reference; $permalink_structure = get_option('permalink_structure'); $is_plain = empty($permalink_structure); if ($is_plain) { if ($reference === '') { $url = home_url('/'); } else { $url = add_query_arg('yatra_booking_confirmation', $reference, home_url('/')); } } else { $booking_base = trim((string) SettingsService::getBookingBase(), '/'); if ($booking_base === '') { $booking_base = 'book'; } $confirmSeg = trim((string) SettingsService::getPermalinkBases()['booking_flow_confirmation_segment'], '/'); if ($confirmSeg === '') { $confirmSeg = 'confirmation'; } $virtual_base = home_url('/' . $booking_base . '/' . $confirmSeg . '/'); /** * Override the base URL for booking confirmation (before the reference path segment). * Return a non-empty string to use a custom base (e.g. get_permalink( $page_id )). * Default null keeps the pageless virtual URL from Settings → booking base. * * @param string|null $base_url Custom base, or null to use virtual URL. * @param string $reference Booking reference (may be empty). */ $base_url = apply_filters('yatra_booking_confirmation_base_url', null, $reference); if (!is_string($base_url) || $base_url === '') { $base_url = $virtual_base; } if ($reference === '') { $url = trailingslashit($base_url); } else { $url = trailingslashit($base_url) . $reference . '/'; } } /** * Filter the booking confirmation URL. * * @param string $url Built URL. * @param string $reference Booking reference (may be empty). */ return (string) apply_filters('yatra_booking_confirmation_url', $url, $reference); } /** * Front-end URL to verify a customer email (checkout registration / account). * * Pretty permalinks: /yatra-verify-email/{token}/ (rewrite + query var). * Plain permalinks: ?yatra_verify_email={token} on the home URL (same as {@see \Yatra\Core\Routing\PermalinkCanonical}). * * @param string $secure_token URL-safe token (base64-derived; only [A-Za-z0-9_-] used in the path/query). */ function yatra_get_email_verification_url(string $secure_token): string { $t = preg_replace('/[^a-zA-Z0-9_-]/', '', (string) $secure_token) ?? ''; if ($t === '') { return home_url('/'); } $permalink_structure = get_option('permalink_structure'); $is_plain = empty($permalink_structure); if ($is_plain) { $url = add_query_arg('yatra_verify_email', $t, home_url('/')); } else { $prefix = SettingsService::getPermalinkBases()['email_verification_prefix']; $url = trailingslashit(home_url('/' . $prefix . '/' . $t . '/')); } /** * Filter the customer email verification URL. * * @param string $url Full verification URL. * @param string $token Sanitized token segment. */ return (string) apply_filters('yatra_email_verification_url', $url, $t); } /** * ============================================ * ARCHIVE LISTING (plain permalinks pagination) * ============================================ */ /** * Items per page from WordPress Reading settings ("Blog pages show at most"). * Used for Yatra front-end listings (trips, taxonomies, activity/destination/category archives). * * @return int At least 1. */ function yatra_get_posts_per_page(): int { $n = absint((int) get_option('posts_per_page', 10)); return (int) apply_filters('yatra_posts_per_page', max(1, $n)); } /** * Current page number for Yatra archive templates (activity, destination, trip category). * Handles plain URLs where WordPress may use {@see 'paged'} or {@see 'page'} on the front page. */ function yatra_get_archive_listing_paged(): int { if (isset($_GET['paged']) && $_GET['paged'] !== '') { return max(1, absint(wp_unslash($_GET['paged']))); } if (!empty($_GET['yatra_page']) && isset($_GET['page']) && $_GET['page'] !== '') { return max(1, absint(wp_unslash($_GET['page']))); } $p = (int) get_query_var('paged'); if ($p > 0) { return max(1, $p); } $p = (int) get_query_var('page'); return max(1, $p); } /** * Result summary for destination / activity / trip-category browse pages (parity with trip grid header). * * @param string $items_label Plural noun, e.g. translated "destinations". */ function yatra_archive_browse_results_line(int $start, int $end, int $total, int $page, int $pages, string $items_label): string { if ($total <= 0) { return ''; } return sprintf( /* translators: 1–2: range, 3: total, 4: item type, 5–6: pagination */ __('Showing %1$d–%2$d of %3$d %4$s (page %5$d of %6$d)', 'yatra'), $start, $end, $total, $items_label, $page, $pages ); } /** * Request path (leading slash, no query string) for same-page links. Strips /page/N/ pagination segments. */ function yatra_get_current_request_path_for_query_urls(): string { $request_uri = isset($_SERVER['REQUEST_URI']) ? (string) wp_unslash($_SERVER['REQUEST_URI']) : '/'; $base_path = strtok($request_uri, '?') ?: '/'; $base_path = rtrim((string) $base_path, '/'); $base_path = preg_replace('#/page/[0-9]+#', '', $base_path); $base_path = rtrim($base_path, '/'); if ($base_path === '') { return '/'; } return $base_path[0] === '/' ? $base_path : '/' . $base_path; } /** * Full URL for the same archive request with a different page (preserves yatra_page and other args). * Uses the current request path so /destination/, /activity/, /trip-category/ stay on the same listing. */ function yatra_build_archive_listing_url(int $page_num): string { $params = !empty($_GET) && is_array($_GET) ? wp_unslash($_GET) : []; $qvYatra = (string) get_query_var('yatra_page'); if ($qvYatra !== '' && (!isset($params['yatra_page']) || $params['yatra_page'] === '')) { $params['yatra_page'] = $qvYatra; } if (!empty($params['yatra_page']) || isset($params['yatra_trip'])) { unset($params['page']); } if ($page_num > 1) { $params['paged'] = (string) $page_num; } else { unset($params['paged'], $params['page']); } $path = yatra_get_current_request_path_for_query_urls(); $query = http_build_query($params); return esc_url($path . ($query !== '' ? '?' . $query : '')); } /** * Same request path with a different paged query arg (strips an existing /page/N/ segment first). * For taxonomy trip lists and other templates not rooted at home_url('/'). */ function yatra_build_current_request_paged_url(int $page_num): string { $page_num = max(1, $page_num); $params = !empty($_GET) && is_array($_GET) ? wp_unslash($_GET) : []; if ($page_num > 1) { $params['paged'] = (string) $page_num; } else { unset($params['paged'], $params['page']); } $path = yatra_get_current_request_path_for_query_urls(); $query = http_build_query($params); return esc_url($path . ($query !== '' ? '?' . $query : '')); } /** * Same request path with trip sort (TripRepository / TripListingService). Resets pagination. * * @param string $sort Allowed: '' (recommended), most_popular, price_low, price_high, rating_high, duration_short, duration_long. */ function yatra_build_current_request_sort_url(string $sort): string { $allowed = ['', 'most_popular', 'price_low', 'price_high', 'rating_high', 'duration_short', 'duration_long']; if (!in_array($sort, $allowed, true)) { $sort = ''; } $params = !empty($_GET) && is_array($_GET) ? wp_unslash($_GET) : []; unset($params['paged'], $params['page']); if ($sort !== '') { $params['sort'] = $sort; } else { unset($params['sort']); } $path = yatra_get_current_request_path_for_query_urls(); $query = http_build_query($params); return esc_url($path . ($query !== '' ? '?' . $query : '')); } /** * Compare two archive listing rows (activity, destination, or category) by sort key. */ function yatra_compare_archive_listing_row_pair(object $a, object $b, string $sort): int { $nameA = isset($a->name) ? strtolower((string) $a->name) : ''; $nameB = isset($b->name) ? strtolower((string) $b->name) : ''; $tripsA = isset($a->trips_count) ? (int) $a->trips_count : 0; $tripsB = isset($b->trips_count) ? (int) $b->trips_count : 0; $ratingA = isset($a->avg_rating) ? (float) $a->avg_rating : 0.0; $ratingB = isset($b->avg_rating) ? (float) $b->avg_rating : 0.0; switch ($sort) { case 'trips_desc': return $tripsB <=> $tripsA; case 'trips_asc': return $tripsA <=> $tripsB; case 'name_asc': return $nameA <=> $nameB; case 'name_desc': return $nameB <=> $nameA; case 'rating_desc': default: $cmp = $ratingB <=> $ratingA; if (0 === $cmp) { return $tripsB <=> $tripsA; } return $cmp; } } /** * Invokable comparator for {@see yatra_sort_archive_listing_stats_rows()}. * * @internal */ final class Yatra_Archive_Listing_Stats_Comparator { /** @var string */ private $sort; public function __construct(string $sort) { $this->sort = $sort; } /** * @param object $a * @param object $b */ public function __invoke($a, $b): int { return yatra_compare_archive_listing_row_pair($a, $b, $this->sort); } } /** * Sort archive listing rows in place (stats objects from repository). */ function yatra_sort_archive_listing_stats_rows(array &$items, string $sort): void { if (empty($items)) { return; } usort($items, new Yatra_Archive_Listing_Stats_Comparator($sort)); } /** * Sort dropdown URL: same archive, page reset to 1, yatra_sort applied (preserves yatra_page etc.). */ function yatra_build_archive_listing_sort_url(string $yatra_sort): string { $params = !empty($_GET) && is_array($_GET) ? wp_unslash($_GET) : []; unset($params['paged'], $params['page']); if (!empty($params['yatra_page']) || isset($params['yatra_trip'])) { unset($params['page']); } $params['yatra_sort'] = $yatra_sort; $path = yatra_get_current_request_path_for_query_urls(); $query = http_build_query($params); return esc_url($path . ($query !== '' ? '?' . $query : '')); } /** * ============================================ * PERMALINK HELPERS * ============================================ */ /** * Get destination permalink * * @param object|int $destination Destination object with slug property, or destination ID * @return string Destination permalink URL */ function yatra_get_destination_permalink($destination): string { $original = $destination; if (is_numeric($destination)) { global $wpdb; $table = ClassificationsTable::getTableName(); $destination = $wpdb->get_row($wpdb->prepare( "SELECT slug FROM {$table} WHERE id = %d AND type = %s", (int) $destination, ClassificationTypes::DESTINATION )); } $slug = is_object($destination) ? ($destination->slug ?? '') : ''; if (empty($slug)) { return ''; } $base = SettingsService::getDestinationBase(); $permalink_structure = get_option('permalink_structure'); $is_plain = empty($permalink_structure); if ($is_plain) { $key = preg_replace('/[^a-z0-9_-]/i', '', $base) ?: 'destination'; $url = add_query_arg([$key => $slug], home_url('/')); } else { $url = home_url('/' . $base . '/' . $slug . '/'); } /** @var string $url Override full destination URL or path (plain/pretty handled above). Third arg: slug. */ return (string) apply_filters('yatra_destination_permalink', $url, $original, $slug); } /** * Get activity permalink * * @param object|int $activity Activity object with slug property, or activity ID * @return string Activity permalink URL */ function yatra_get_activity_permalink($activity): string { $original = $activity; if (is_numeric($activity)) { global $wpdb; $table = ClassificationsTable::getTableName(); $activity = $wpdb->get_row($wpdb->prepare( "SELECT slug FROM {$table} WHERE id = %d AND type = %s", (int) $activity, ClassificationTypes::ACTIVITY )); } $slug = is_object($activity) ? ($activity->slug ?? '') : ''; if (empty($slug)) { return ''; } $base = SettingsService::getActivityBase(); $permalink_structure = get_option('permalink_structure'); $is_plain = empty($permalink_structure); if ($is_plain) { $key = preg_replace('/[^a-z0-9_-]/i', '', $base) ?: 'activity'; $url = add_query_arg([$key => $slug], home_url('/')); } else { $url = home_url('/' . $base . '/' . $slug . '/'); } /** @var string $url Override full activity URL. Third arg: slug. */ return (string) apply_filters('yatra_activity_permalink', $url, $original, $slug); } /** * Get trip category permalink * * @param object|int $category Category object with slug property, or category ID * @return string Category permalink URL */ function yatra_get_category_permalink($category): string { $original = $category; if (is_numeric($category)) { global $wpdb; $table = ClassificationsTable::getTableName(); $category = $wpdb->get_row($wpdb->prepare( "SELECT slug FROM {$table} WHERE id = %d AND type = %s", (int) $category, ClassificationTypes::CATEGORY )); } $slug = is_object($category) ? ($category->slug ?? '') : ''; if (empty($slug)) { return ''; } $base = SettingsService::getTripCategoryBase(); $permalink_structure = get_option('permalink_structure'); $is_plain = empty($permalink_structure); if ($is_plain) { $key = preg_replace('/[^a-z0-9_-]/i', '', $base) ?: 'trip-category'; $url = add_query_arg([$key => $slug], home_url('/')); } else { $url = home_url('/' . $base . '/' . $slug . '/'); } /** @var string $url Override full trip-category URL. Third arg: slug. */ return (string) apply_filters('yatra_category_permalink', $url, $original, $slug); } /** * Get trip permalink * * @param object|int $trip Trip object with slug property, or trip ID * @return string Trip permalink URL */ function yatra_get_trip_permalink($trip): string { $original = $trip; if (is_numeric($trip)) { global $wpdb; $table = TripsTable::getTableName(); $trip = $wpdb->get_row($wpdb->prepare( "SELECT slug FROM {$table} WHERE id = %d", (int) $trip )); } $slug = is_object($trip) ? ($trip->slug ?? '') : ''; if (empty($slug)) { return ''; } $base = SettingsService::getTripBase(); $permalink_structure = get_option('permalink_structure'); $is_plain = empty($permalink_structure); if ($is_plain) { $key = preg_replace('/[^a-z0-9_-]/i', '', $base) ?: 'trip'; $url = add_query_arg([$key => $slug], home_url('/')); } else { $url = home_url('/' . $base . '/' . $slug . '/'); } /** @var string $url Override full trip URL. Third arg: slug. */ return (string) apply_filters('yatra_trip_permalink', $url, $original, $slug); } /** * Canonical URL for the trip archive / filter listing (respects Settings trip base). * Plain permalinks use ?yatra_page={base}; pretty permalinks use /{base}/. */ function yatra_get_trip_listing_url(): string { $base = SettingsService::getTripBase(); $base = preg_replace('/[^a-zA-Z0-9_-]/', '', (string) $base) ?: 'trip'; $permalink_structure = (string) get_option('permalink_structure', ''); if ($permalink_structure === '') { $url = esc_url(add_query_arg('yatra_page', $base, home_url('/'))); } else { $url = trailingslashit(home_url('/' . $base . '/')); } return (string) apply_filters('yatra_trip_listing_url', $url, $base); } /** * Canonical URL for browse-all taxonomy listings (destinations, activities, trip categories). * Plain permalinks use ?yatra_page={base}; pretty permalinks use /{base}/. * * @param string $listing_type One of: destination, activity, category */ function yatra_get_taxonomy_listing_url(string $listing_type): string { $map = [ 'destination' => SettingsService::getDestinationBase(), 'activity' => SettingsService::getActivityBase(), 'category' => SettingsService::getTripCategoryBase(), ]; $base = $map[$listing_type] ?? ''; $base = preg_replace('/[^a-zA-Z0-9_-]/', '', (string) $base) ?: 'destination'; $permalink_structure = (string) get_option('permalink_structure', ''); if ($permalink_structure === '') { $url = esc_url(add_query_arg('yatra_page', $base, home_url('/'))); } else { $url = trailingslashit(home_url('/' . $base . '/')); } return (string) apply_filters('yatra_taxonomy_listing_url', $url, $listing_type, $base); } /** * Decode trips.price_types for listing-card logic (DB may store JSON string or array). * * @return array> */ function yatra_trip_listing_decode_price_types(object $trip): array { $pts = $trip->price_types ?? null; if (is_string($pts) && $pts !== '') { $decoded = json_decode($pts, true); $pts = is_array($decoded) ? $decoded : []; } elseif (!is_array($pts)) { $pts = []; } if ($pts === [] && method_exists($trip, 'getPriceTypes')) { $got = $trip->getPriceTypes(); $pts = is_array($got) ? $got : []; } return $pts; } /** * Lowercase keys for traveler tier labels (used to strip mis-tagged classifications). * * @return array */ function yatra_trip_listing_traveler_tier_label_keys(object $trip): array { if (($trip->pricing_type ?? '') !== 'traveler_based') { return []; } $keys = []; foreach (yatra_trip_listing_decode_price_types($trip) as $pt) { if (!is_array($pt)) { continue; } foreach (['label', 'category_label', 'title'] as $k) { if (!empty($pt[$k]) && is_string($pt[$k])) { $t = strtolower(trim($pt[$k])); if ($t !== '') { $keys[$t] = true; } break; } } } return $keys; } /** * Ordered unique labels for the listing card “Traveler types” row. * * @return list */ function yatra_trip_listing_traveler_type_labels_for_card(object $trip): array { if (($trip->pricing_type ?? '') !== 'traveler_based') { return []; } $labels = []; $seen = []; foreach (yatra_trip_listing_decode_price_types($trip) as $pt) { if (!is_array($pt)) { continue; } foreach (['label', 'category_label', 'title'] as $k) { if (!empty($pt[$k]) && is_string($pt[$k])) { $lab = trim($pt[$k]); if ($lab === '') { break; } $lk = strtolower($lab); if (!isset($seen[$lk])) { $seen[$lk] = true; $labels[] = $lab; } break; } } } return $labels; } /** * Format start → end for listing cards; avoids repeating the same country when both * strings are "City, Country". */ function yatra_format_trip_listing_route_line(string $start, string $end): string { $start = trim($start); $end = trim($end); if ($start === '') { return $end; } if ($end === '') { return $start; } if (strcasecmp($start, $end) === 0) { return $start; } if (strpos($start, ',') !== false && strpos($end, ',') !== false) { $s_parts = array_map('trim', explode(',', $start, 2)); $e_parts = array_map('trim', explode(',', $end, 2)); if (count($s_parts) === 2 && count($e_parts) === 2 && strcasecmp($s_parts[1], $e_parts[1]) === 0) { return $s_parts[0] . ' → ' . $e_parts[0] . ', ' . $s_parts[1]; } } return $start . ' → ' . $end; } /** * Human label for trip_type column (listing card meta). */ function yatra_trip_listing_trip_type_label(?string $trip_type): string { $t = (string) $trip_type; $map = [ 'single_day' => __('Single day', 'yatra'), 'multi_day' => __('Multi-day', 'yatra'), 'flexible' => __('Flexible', 'yatra'), ]; return $map[$t] ?? ''; } /** * Rating block for listing cards: prefers SQL aggregates (average_rating, review_count) * when the hydrated reviews array is empty. * * @param array{has_rating: bool, average_rating: float, review_count: int, formatted_rating: string} $from_reviews * @return array{has_rating: bool, average_rating: float, review_count: int, formatted_rating: string} */ function yatra_trip_listing_card_rating_data(object $trip, array $from_reviews): array { $has = !empty($from_reviews['has_rating']); $avg = (float) ($from_reviews['average_rating'] ?? 0); $cnt = (int) ($from_reviews['review_count'] ?? 0); $fmt = (string) ($from_reviews['formatted_rating'] ?? '0.0'); if ($cnt === 0 || !$has || $avg <= 0) { $q_avg = isset($trip->average_rating) ? (float) $trip->average_rating : null; $q_cnt = isset($trip->review_count) ? (int) $trip->review_count : null; if (($q_cnt === null || $q_cnt === 0) && isset($trip->reviews_count)) { $q_cnt = (int) $trip->reviews_count; } if ($q_cnt !== null && $q_cnt > 0 && $q_avg !== null && $q_avg > 0) { $avg = round($q_avg, 1); $cnt = $q_cnt; $fmt = number_format($avg, 1); $has = true; } } return [ 'has_rating' => $has && $avg > 0 && $cnt > 0, 'average_rating' => $avg, 'review_count' => $cnt, 'formatted_rating' => $fmt, ]; } /** * Avoid repeating the same classification label in the destination, activity, and category * rows on listing cards (traveler tier labels wrongly linked as classifications, or same * term attached in multiple roles). * * @param array $destinations * @param array $activities * @param array $categories * @return array{0: array, 1: array, 2: array} */ function yatra_trip_listing_filter_classification_duplicates(array $destinations, array $activities, array $categories, object $trip): array { $tier_keys = yatra_trip_listing_traveler_tier_label_keys($trip); $strip_tiers = static function (array $items) use ($tier_keys): array { if ($tier_keys === []) { return $items; } return array_values(array_filter($items, static function ($item) use ($tier_keys) { $n = strtolower(trim((string) ($item->name ?? ''))); return $n === '' || !isset($tier_keys[$n]); })); }; $destinations = $strip_tiers($destinations); $activities = $strip_tiers($activities); $categories = $strip_tiers($categories); $seen = []; $dedupe = static function (array $items) use (&$seen): array { $out = []; foreach ($items as $item) { $n = strtolower(trim((string) ($item->name ?? ''))); if ($n === '') { $out[] = $item; continue; } if (isset($seen[$n])) { continue; } $seen[$n] = true; $out[] = $item; } return $out; }; $destinations = $dedupe($destinations); $activities = $dedupe($activities); $categories = $dedupe($categories); return [$destinations, $activities, $categories]; } /** * Check if we're on a trip listing page * * @return bool True if on a trip listing page */ function yatra_is_trip_listing(): bool { global $yatra_trip_list; // Check for trip list context (base trip listing page) if (!empty($yatra_trip_list)) { return true; } // Check if we're on the main trips listing page $trip_base = SettingsService::getTripBase(); $request_uri = $_SERVER['REQUEST_URI'] ?? ''; $parsed_url = parse_url($request_uri, PHP_URL_PATH); if ($parsed_url && strpos($parsed_url, '/' . $trip_base) === 0) { $path_parts = array_values(array_filter(explode('/', trim($parsed_url, '/')))); if ($path_parts === [] || ($path_parts[0] ?? '') !== $trip_base) { return false; } // /trip/ or /trip/page/2/ (WordPress paged archives) if (count($path_parts) === 1) { return true; } if (count($path_parts) === 3 && ($path_parts[1] ?? '') === 'page' && ctype_digit((string) ($path_parts[2] ?? ''))) { return true; } } return false; } /** * Check if we're on a taxonomy page (destination, activity, category) * * @return bool True if on a taxonomy page */ function yatra_is_taxonomy_page(): bool { global $yatra_taxonomy_data; return !empty($yatra_taxonomy_data); } /** * Check if we're on an activity listing page * * @return bool True if on an activity listing page */ function yatra_is_activity_listing(): bool { return isset($_GET['yatra_page_type']) && $_GET['yatra_page_type'] === 'activities'; } /** * Check if we're on a destination listing page * * @return bool True if on a destination listing page */ function yatra_is_destination_listing(): bool { return isset($_GET['yatra_page_type']) && $_GET['yatra_page_type'] === 'destinations'; } /** * Check if we're on an account page * * @return bool True if on an account page */ function yatra_is_account_page(): bool { if (!empty($GLOBALS['yatra_loading_react_account_page'])) { return true; } if ((string) get_query_var('yatra_account_page') !== '') { return true; } global $post; if ($post && function_exists('has_shortcode') && isset($post->post_content) && has_shortcode((string) $post->post_content, 'yatra_my_account')) { return true; } if (!$post) { return false; } $accountPageId = get_option('yatra_my_account_page'); return $accountPageId && (int) $post->ID === (int) $accountPageId; } /** * Get difficulty level permalink * * @param object|int $difficulty Difficulty object with slug property, or difficulty ID * @return string Difficulty permalink URL */ function yatra_get_difficulty_permalink($difficulty): string { if (is_numeric($difficulty)) { global $wpdb; $table = ClassificationsTable::getTableName(); $difficulty = $wpdb->get_row($wpdb->prepare( "SELECT slug FROM {$table} WHERE id = %d AND type = %s", (int) $difficulty, ClassificationTypes::DIFFICULTY )); } $slug = is_object($difficulty) ? ($difficulty->slug ?? '') : ''; if (empty($slug)) { return ''; } $base = SettingsService::getString('difficulty_base', 'difficulty'); return home_url('/' . $base . '/' . $slug . '/'); } /** * Load a template file with theme override support * * This function allows themes to override plugin templates by placing them in: * theme/yatra/template-name.php * * If no theme override exists, loads from plugin templates directory. * * @param string $template_name Template file name (without .php extension) * @param array $args Arguments to extract and make available in template * @param string $template_path Template path within plugin (default: 'templates/') * @param array $data Alternative data array (won't be extracted, available as $data) * @return void */ function yatra_get_template(string $template_name, array $args = [], string $template_path = 'templates/', array $data = []): void { $template_name = ltrim($template_name, '/'); // Check if theme has override $theme_template = locate_template([ 'yatra/' . $template_name . '.php', 'yatra/' . $template_name ]); if ($theme_template) { // Load from theme $template_file = $theme_template; } else { // Load from plugin $template_file = YATRA_PLUGIN_PATH . ltrim($template_path, '/') . '/' . $template_name . '.php'; } // Extract arguments to make them available as individual variables if (!empty($args)) { extract($args); } // Make data available as $data array (not extracted) if (!empty($data)) { $data = $data; } // Include the template if (file_exists($template_file)) { include $template_file; } } /** * Enqueue single trip scripts and styles * * @return void */ function yatra_enqueue_single_trip_scripts(): void { // Only enqueue on single trip pages if (!is_single() || get_post_type() !== 'trip') { return; } // Enqueue the single trip JavaScript wp_enqueue_script( 'yatra-single-trip', YATRA_PLUGIN_URL . 'assets/js/single-trip.js', ['jquery', 'yatra-trip'], YATRA_VERSION, true ); // Localize script data global $trip; if ($trip) { wp_localize_script( 'yatra-single-trip', 'yatraSingleTripData', [ 'tripId' => (int) $trip->id, 'basePrice' => (float) ($trip->base_price ?? 0), 'currencySymbol' => yatra_get_currency_symbol(\Yatra\Services\SettingsService::getCurrency()), 'apiUrls' => [ 'groupDiscounts' => rest_url('yatra/v1/discounts/group-discounts') ] ] ); } } /** * Calculate base price for single trip display using CalculationService * * @param object $trip Trip object * @return array Pricing data including base_price, has_availability, has_traveler_pricing, pricing_type */ function yatra_single_trip_calculate_base_price($trip) { // Check if availability dates exist (PRIORITY) $has_availability = !empty($trip->availability_dates) && is_array($trip->availability_dates) && count($trip->availability_dates) > 0; // Determine pricing type from trip settings $pricing_type = $trip->pricing_type ?? 'regular'; $has_traveler_pricing = ($pricing_type === 'traveler_based' && !empty($trip->price_types)); // Use CalculationService for consistent pricing $calculationService = new \Yatra\Services\CalculationService(); // Determine base price using CalculationService logic $trip_price = 0; if ($has_availability) { // Page-load pricing priority (traveler-based): // - If a default category is marked at trip-level, use that as the base price. // - Otherwise fall back to lowest price across availability (legacy behavior). $default_trip_price = 0.0; if ($has_traveler_pricing && !empty($trip->price_types) && is_array($trip->price_types)) { $default_price_type = null; foreach ($trip->price_types as $pt) { if (is_array($pt)) { $pt = (object) $pt; } if (!empty($pt->is_default)) { $default_price_type = $pt; break; } } if ($default_price_type) { $default_trip_price = (float) ($default_price_type->effective_price ?? $default_price_type->discounted_price ?? $default_price_type->original_price ?? 0); } } if ($default_trip_price > 0) { $trip_price = $default_trip_price; } else { // Get the lowest price from availability dates $min_price = PHP_FLOAT_MAX; foreach ($trip->availability_dates as $avail) { $avail_price = $avail->effective_price ?? $avail->original_price ?? 0; if ($avail_price > 0 && $avail_price < $min_price) { $min_price = $avail_price; } // Also check price_types within availability if traveler-based if (!empty($avail->price_types) && is_array($avail->price_types)) { foreach ($avail->price_types as $pt) { $pt = (object)$pt; $pt_price = (float)($pt->effective_price ?? $pt->discounted_price ?? $pt->original_price ?? 0); if ($pt_price > 0 && $pt_price < $min_price) { $min_price = $pt_price; } } } } // If no price found from availability, check traveler-based pricing if ($min_price >= PHP_FLOAT_MAX && $has_traveler_pricing) { foreach ($trip->price_types as $pt) { $pt = is_array($pt) ? (object) $pt : $pt; $pt_price = (float)($pt->effective_price ?? $pt->discounted_price ?? $pt->original_price ?? 0); if ($pt_price > 0 && $pt_price < $min_price) { $min_price = $pt_price; } } } $trip_price = ($min_price < PHP_FLOAT_MAX) ? $min_price : ($trip->sale_price ?: $trip->original_price); } } elseif ($has_traveler_pricing) { // Get default or first traveler category price $default_price_type = null; foreach ($trip->price_types as $pt) { if (!empty($pt->is_default)) { $default_price_type = $pt; break; } } if (!$default_price_type && !empty($trip->price_types)) { $default_price_type = $trip->price_types[0]; } // Get the price from the price type - check multiple possible fields if ($default_price_type) { $trip_price = 0; // Try effective_price first, then discounted_price, then original_price if (!empty($default_price_type->effective_price) && $default_price_type->effective_price > 0) { $trip_price = (float)$default_price_type->effective_price; } elseif (!empty($default_price_type->discounted_price) && $default_price_type->discounted_price > 0) { $trip_price = (float)$default_price_type->discounted_price; } elseif (!empty($default_price_type->original_price) && $default_price_type->original_price > 0) { $trip_price = (float)$default_price_type->original_price; } elseif (!empty($default_price_type->sale_price) && $default_price_type->sale_price > 0) { $trip_price = (float)$default_price_type->sale_price; } // If still no price, try to get the minimum from all price types if ($trip_price <= 0) { foreach ($trip->price_types as $pt) { $pt_price = (float)($pt->effective_price ?? $pt->discounted_price ?? $pt->original_price ?? 0); if ($pt_price > 0 && ($trip_price <= 0 || $pt_price < $trip_price)) { $trip_price = $pt_price; } } } } else { $trip_price = $trip->sale_price ?: $trip->original_price; } } else { // Regular pricing $trip_price = $trip->sale_price > 0 ? $trip->sale_price : $trip->original_price; } // Apply CalculationService filter for dynamic pricing (pro plugins) $base_price = apply_filters('yatra_calculate_base_amount', $trip_price, [ 'trip_price' => $trip_price, 'travelers_count' => 1, 'traveler_counts' => ['default' => 1], 'pricing_type' => $pricing_type, 'price_types' => $trip->price_types ?? [], 'trip_id' => $trip->id ?? 0 ]); return [ 'base_price' => $base_price, 'has_availability' => $has_availability, 'has_traveler_pricing' => $has_traveler_pricing, 'pricing_type' => $pricing_type ]; } /** * Get group discounts data for single trip * * @param int $trip_id Trip ID * @return array Group discounts data including has_group_discounts and group_discounts_data */ function yatra_single_trip_get_group_discounts($trip_id) { $has_group_discounts = false; $group_discounts_data = []; $trip_id = (int) $trip_id; if ($trip_id <= 0) { return [ 'has_group_discounts' => false, 'group_discounts_data' => [], ]; } try { // Direct controller path avoids rest_do_request / loopback issues on single-trip templates. if (class_exists(\Yatra\Controllers\DiscountController::class)) { $ctrl = new \Yatra\Controllers\DiscountController(); $payload = $ctrl->getPublicGroupDiscountDiscoverabilityForTrip($trip_id); $discounts = isset($payload['discounts']) && is_array($payload['discounts']) ? $payload['discounts'] : []; if (!empty($payload['has_group_discounts']) && $discounts !== []) { return [ 'has_group_discounts' => true, 'group_discounts_data' => $discounts, ]; } } $row = null; // Fallback: internal REST then HTTP (e.g. if controller unavailable). if (class_exists('\WP_REST_Request') && function_exists('rest_do_request')) { $request = new \WP_REST_Request('GET', '/yatra/v1/discounts/group-discounts'); $request->set_param('trip_ids', [$trip_id]); $rest_response = rest_do_request($request); if ($rest_response instanceof \WP_REST_Response && $rest_response->get_status() === 200) { $row = yatra_single_trip_parse_group_discounts_payload($rest_response->get_data(), $trip_id); } } if (!is_array($row)) { $api_url = add_query_arg( ['trip_ids' => [$trip_id]], rest_url('yatra/v1/discounts/group-discounts') ); $response = wp_remote_get($api_url, [ 'timeout' => 6, 'headers' => [ 'Accept' => 'application/json', ], ]); if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) { $data = json_decode(wp_remote_retrieve_body($response), true); $row = yatra_single_trip_parse_group_discounts_payload($data, $trip_id); } } if (is_array($row) && !empty($row['has_group_discounts']) && !empty($row['discounts']) && is_array($row['discounts'])) { $has_group_discounts = true; $group_discounts_data = $row['discounts']; } } catch (Exception $e) { $has_group_discounts = false; } return [ 'has_group_discounts' => $has_group_discounts, 'group_discounts_data' => $group_discounts_data, ]; } /** * Extract the per-trip object from a group-discounts REST payload (handles optional wrappers). * * @param mixed $data * @return array|null */ function yatra_single_trip_parse_group_discounts_payload($data, int $trip_id): ?array { if (!is_array($data)) { return null; } if (isset($data['data']) && is_array($data['data'])) { $data = $data['data']; } $keyStr = (string) $trip_id; $row = $data[$trip_id] ?? $data[$keyStr] ?? null; return is_array($row) ? $row : null; } /** * Payload for single-trip booking UI JS (sidebar date/traveler pricing + group tiers). * Kept in yatraTripData instead of large HTML data-* attributes on .yatra-booking-card. * * @param object $trip Trip model * @return array{pricingType: string, sidebarAvailability: array>, sidebarGroupDiscounts: array>} */ function yatra_single_trip_get_client_booking_payload($trip): array { $empty = [ 'pricingType' => 'regular', 'sidebarAvailability' => [], 'sidebarGroupDiscounts' => [], ]; if (!is_object($trip) || empty($trip->id)) { return $empty; } $pricing_data = function_exists('yatra_single_trip_calculate_base_price') ? yatra_single_trip_calculate_base_price($trip) : ['has_availability' => false, 'pricing_type' => $trip->pricing_type ?? 'regular']; $pricing_type = (string) ($pricing_data['pricing_type'] ?? ($trip->pricing_type ?? 'regular')); $has_availability = !empty($pricing_data['has_availability']); $availability = []; if ($has_availability && method_exists($trip, 'getAvailabilityDates')) { foreach ($trip->getAvailabilityDates() as $avail) { if (!is_object($avail)) { continue; } $price_types_raw = !empty($avail->price_types) && is_array($avail->price_types) ? $avail->price_types : []; $price_types = []; foreach ($price_types_raw as $pt) { if (is_object($pt)) { $decoded = json_decode(wp_json_encode($pt), true); $price_types[] = is_array($decoded) ? $decoded : []; } elseif (is_array($pt)) { $price_types[] = $pt; } } $availability[] = [ 'id' => (int) ($avail->id ?? 0), 'date' => $avail->departure_date ?? '', 'departure_date' => $avail->departure_date ?? '', 'return_date' => (isset($avail->return_date) && $avail->return_date !== '') ? $avail->return_date : (isset($avail->arrival_date) ? $avail->arrival_date : null), 'price' => $avail->effective_price ?? $avail->original_price ?? 0, 'original_price' => $avail->original_price ?? 0, 'discounted_price' => $avail->discounted_price ?? null, 'seats_available' => $avail->seats_available ?? 0, 'seats_total' => $avail->seats_total ?? 0, 'status' => $avail->status ?? '', 'is_limited' => (bool) ($avail->is_limited ?? false), 'is_sold_out' => (bool) ($avail->is_sold_out ?? false), 'pricing_type' => $price_types !== [] ? 'traveler_based' : $pricing_type, 'price_types' => $price_types, ]; } } $sidebar_group_discounts = []; if (function_exists('yatra_single_trip_get_group_discounts')) { $gd = yatra_single_trip_get_group_discounts((int) $trip->id); $cards = isset($gd['group_discounts_data']) && is_array($gd['group_discounts_data']) ? $gd['group_discounts_data'] : []; $sidebar_group_discounts = apply_filters('yatra_advanced_discount_enabled', false) ? $cards : []; $sidebar_group_discounts = array_values(array_map(static function ($row) { if (is_object($row)) { $decoded = json_decode(wp_json_encode($row), true); return is_array($decoded) ? $decoded : []; } return $row; }, $sidebar_group_discounts)); } return [ 'pricingType' => $pricing_type, 'sidebarAvailability' => $availability, 'sidebarGroupDiscounts' => $sidebar_group_discounts, ]; } // Hook into WordPress enqueue system add_action('wp_enqueue_scripts', 'yatra_enqueue_single_trip_scripts'); // Yatra page type detection functions if (!function_exists('yatra_is_trip_page')) { function yatra_is_trip_page() { global $trip; return isset($trip) && !empty($trip); } } if (!function_exists('yatra_is_destination_page')) { function yatra_is_destination_page() { global $destination, $yatra_taxonomy_data; // Check direct global first if (isset($destination) && !empty($destination)) { return true; } // Check taxonomy data if (isset($yatra_taxonomy_data) && !empty($yatra_taxonomy_data) && $yatra_taxonomy_data->type === 'destination') { return true; } return false; } } if (!function_exists('yatra_is_activity_page')) { function yatra_is_activity_page() { global $activity, $yatra_taxonomy_data; // Check direct global first if (isset($activity) && !empty($activity)) { return true; } // Check taxonomy data if (isset($yatra_taxonomy_data) && !empty($yatra_taxonomy_data) && $yatra_taxonomy_data->type === 'activity') { return true; } return false; } } if (!function_exists('yatra_is_category_page')) { function yatra_is_category_page() { global $category, $yatra_taxonomy_data; // Check direct global first if (isset($category) && !empty($category)) { return true; } // Check taxonomy data if (isset($yatra_taxonomy_data) && !empty($yatra_taxonomy_data) && $yatra_taxonomy_data->type === 'category') { return true; } return false; } } if (!function_exists('yatra_is_trip_archive_page')) { function yatra_is_trip_archive_page() { $current_url = $_SERVER['REQUEST_URI'] ?? ''; $current_path = parse_url($current_url, PHP_URL_PATH) ?? ''; $trip_base = \Yatra\Services\SettingsService::getTripBase(); // Check for both /trip/ and /trip patterns $pattern1 = '/' . $trip_base . '/'; $pattern2 = '/' . $trip_base; return (strpos($current_path, $pattern1) !== false || $current_path === $pattern2) && !yatra_is_trip_page(); } } // Yatra only has trip archive pages - no destination/activity/category archive pages if (!function_exists('yatra_is_listing_page')) { function yatra_is_listing_page() { $current_url = $_SERVER['REQUEST_URI'] ?? ''; $current_path = parse_url($current_url, PHP_URL_PATH) ?? ''; return strpos($current_path, '/listing-') !== false; } } if (!function_exists('yatra_is_yatra_page')) { function yatra_is_yatra_page() { return yatra_is_trip_page() || yatra_is_destination_page() || yatra_is_activity_page() || yatra_is_category_page() || yatra_is_trip_archive_page() || yatra_is_listing_page(); } } if ( ! function_exists( 'yatra_get_header' ) ) { function yatra_get_header( $header_name = null ) { global $wp_version; // When the template is being rendered as the body of the yatra/page-content // server block inside a block-template canvas, the canvas already emits the // doctype/html/head/body and the site header template part. Re-emitting them // here would nest / and duplicate the header — so we no-op. if ( class_exists( '\\Yatra\\Core\\Template\\FseTemplates' ) && \Yatra\Core\Template\FseTemplates::isRenderingInsideCanvas() ) { return; } if ( version_compare( $wp_version, '5.9', '>=' ) && function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ) { /* * Full-site editing themes often omit add_theme_support( 'title-tag' ); the document title is * injected via template canvas using _block_template_render_title_tag (unconditional). Yatra * renders this minimal head instead of canvas, so _wp_render_title_tag would no-op and the * page would have no . Mirror canvas: print title here and drop duplicate core hooks. */ remove_action( 'wp_head', '_wp_render_title_tag', 1 ); remove_action( 'wp_head', '_block_template_render_title_tag', 1 ); ?> <!doctype html> <html <?php language_attributes(); ?>> <head> <meta charset="<?php bloginfo( 'charset' ); ?>"> <title><?php echo esc_html( wp_get_document_title() ); ?> >
/. No-op here to avoid duplicates. if ( class_exists( '\\Yatra\\Core\\Template\\FseTemplates' ) && \Yatra\Core\Template\FseTemplates::isRenderingInsideCanvas() ) { return; } if ( version_compare( $wp_version, '5.9', '>=' ) && function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ) { ?>
'; return; } } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- markup built from sanitized picker / SVG registry echo yatra_stored_picker_icon_markup($icon_data, $default_icon, $css_class); } } if (!function_exists('yatra_listing_sidebar_filter_visible_cap')) { /** * How many sidebar checkbox rows to show before "Show more" on the trip listing. * * Filter: {@see 'yatra_listing_sidebar_filter_visible_count'} — default 8, clamped 3–40. * * @return int */ function yatra_listing_sidebar_filter_visible_cap(): int { $n = (int) apply_filters('yatra_listing_sidebar_filter_visible_count', 8); return max(3, min(40, $n)); } } if (!function_exists('yatra_wishlist_enabled')) { /** * Whether wishlist UI and REST should be active (Yatra Pro + setting). */ function yatra_wishlist_enabled(): bool { return \Yatra\Services\SettingsService::wishlistEnabled(); } } if (!function_exists('yatra_usage_track_event')) { /** * Record an anonymous product telemetry event (requires opt-in). * * @param string $event Event key (sanitized). * @param int $delta Counter increment. */ function yatra_usage_track_event(string $event, int $delta = 1): void { if (!class_exists(\Yatra\Services\StatsUsage::class)) { return; } \Yatra\Services\StatsUsage::instance()->record_event($event, $delta); } }