PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
← All changes | app/Controllers/SingleTripController.php +76 -13 3.0.4 → 3.0.16 View file →
@@ -100,10 +100,17 @@
100 100 $slug
101 101 )
102 102 );
103 103
104 - // If trip exists but admin is not logged in, return null to show "not found"
105 - if ($existingTrip && !current_user_can('yatra_edit_trips')) {
104 + // If trip exists but viewer can't preview drafts, hide it.
105 + // Admin fallback so site owners always see drafts even when
106 + // the Team module isn't active and yatra_edit_trips isn't
107 + // on the admin role.
108 + if (
109 + $existingTrip
110 + && !current_user_can('manage_options')
111 + && !current_user_can('yatra_edit_trips')
112 + ) {
106 113 return null;
107 114 }
108 115 }
109 116
@@ -389,13 +396,14 @@
389 396 {
390 397 // Use centralized AvailabilityResolutionService
391 398 $resolutionService = new \Yatra\Services\AvailabilityResolutionService();
392 399
393 - // Get dates for next 12 months
400 + // From today up to the configurable booking horizon (Settings → Booking;
401 + // 12 months unless changed — the previous hard-coded value).
394 402 $fromDate = date('Y-m-d');
395 - $toDate = date('Y-m-d', strtotime('+12 months'));
403 + $toDate = yatra_get_availability_horizon_date($fromDate);
396 404
397 - $availability = $resolutionService->getAllAvailabilityDates($trip_id, $fromDate, $toDate);
405 + $availability = $resolutionService->getAllAvailabilityDates($trip_id, $fromDate, $toDate, \Yatra\Services\SettingsService::isEnabled('show_sold_out'));
398 406
399 407 // Add calculated fields
400 408 foreach ($availability as $avail) {
401 409 // Calculate if limited availability
@@ -459,8 +467,9 @@
459 467 'age_min' => isset($meta['age_min']) ? (int) $meta['age_min'] : null,
460 468 'age_max' => isset($meta['age_max']) ? (int) $meta['age_max'] : null,
461 469 'min_pax' => isset($meta['min_pax']) ? (int) $meta['min_pax'] : null,
462 470 'max_pax' => isset($meta['max_pax']) ? (int) $meta['max_pax'] : null,
471 + 'group_overflow' => isset($meta['group_overflow']) && in_array($meta['group_overflow'], ['block', 'per_block'], true) ? $meta['group_overflow'] : 'block',
463 472 'max_quantity' => isset($meta['max_quantity']) ? (int) $meta['max_quantity'] : null,
464 473 'description' => $meta['description'] ?? '',
465 474 ];
466 475 }
@@ -490,8 +499,9 @@
490 499 'age_min' => $cat ? $cat->age_min : null,
491 500 'age_max' => $cat ? $cat->age_max : null,
492 501 'min_pax' => $cat ? $cat->min_pax : null,
493 502 'max_pax' => $cat ? $cat->max_pax : null,
503 + 'group_overflow' => $cat ? $cat->group_overflow : ($pt['group_overflow'] ?? 'block'),
494 504 'max_quantity' => $cat ? $cat->max_quantity : null,
495 505 'description' => $cat ? $cat->description : ($pt['description'] ?? ''),
496 506 ];
497 507 }
@@ -1085,9 +1095,13 @@
1085 1095 $patterns = [
1086 1096 '/youtube\.com\/watch\?v=([^&]+)/',
1087 1097 '/youtube\.com\/embed\/([^?]+)/',
1088 1098 '/youtu\.be\/([^?]+)/',
1089 - '/youtube\.com\/v\/([^?]+)/'
1099 + '/youtube\.com\/v\/([^?]+)/',
1100 + // Shorts + live URLs (e.g. youtube.com/shorts/XFR9Ti-4RbM?si=...).
1101 + // ID stops at ?, & or / so trailing query params are excluded.
1102 + '/youtube\.com\/shorts\/([^?&\/]+)/',
1103 + '/youtube\.com\/live\/([^?&\/]+)/'
1090 1104 ];
1091 1105
1092 1106 foreach ($patterns as $pattern) {
1093 1107 if (preg_match($pattern, $url, $matches)) {
@@ -1219,13 +1233,19 @@
1219 1233 */
1220 1234 private function getSimilarTrips(object $trip): array
1221 1235 {
1222 1236 $trip_id = (int) $trip->id;
1237 +
1238 + // Hour-based day tours (3.0.14+ column) — guarded so an install whose
1239 + // upgrade ALTER has not run yet keeps rendering similar trips.
1240 + $duration_hours_col = (new \Yatra\Repositories\TripRepository())->hasTripColumn('duration_hours')
1241 + ? ', duration_hours'
1242 + : '';
1223 1243
1224 1244 // Get similar trips based on category or difficulty
1225 1245 $similar = $this->wpdb->get_results(
1226 1246 $this->wpdb->prepare(
1227 - "SELECT id, title, slug, featured_image AS featured_image_id, '' AS featured_image_url, duration_days, duration_nights,
1247 + "SELECT id, title, slug, featured_image AS featured_image_id, '' AS featured_image_url, duration_days, duration_nights{$duration_hours_col},
1228 1248 original_price, sale_price, difficulty_level,
1229 1249 short_description
1230 1250 FROM {$this->table_trips} t
1231 1251 WHERE t.id != %d
@@ -1256,9 +1276,9 @@
1256 1276 // Fallback: Get any published trips if no similar found
1257 1277 if (empty($similar)) {
1258 1278 $similar = $this->wpdb->get_results(
1259 1279 $this->wpdb->prepare(
1260 - "SELECT id, title, slug, featured_image AS featured_image_id, '' AS featured_image_url, duration_days, duration_nights,
1280 + "SELECT id, title, slug, featured_image AS featured_image_id, '' AS featured_image_url, duration_days, duration_nights{$duration_hours_col},
1261 1281 original_price, sale_price, difficulty_level,
1262 1282 short_description
1263 1283 FROM {$this->table_trips}
1264 1284 WHERE id != %d
@@ -1511,13 +1531,20 @@
1511 1531 'included' => !empty($entry->included_items) ? json_decode($entry->included_items, true) : [],
1512 1532 'excluded' => !empty($entry->excluded_items) ? json_decode($entry->excluded_items, true) : [],
1513 1533 'gallery' => !empty($entry->gallery) ? $this->decodeGallery($entry->gallery) : [],
1514 1534 'video_url' => $entry->video_url ?: '',
1535 + // The admin "Notes / Instructions" textarea ("Additional notes
1536 + // or special instructions for this activity") was stored but
1537 + // never reached the public template — the array key was
1538 + // simply absent. Without this, operators saw their notes
1539 + // discarded silently on the live trip page.
1540 + 'notes' => (string) ($entry->notes ?? ''),
1515 1541 ];
1516 1542 }
1517 1543
1518 1544 $itinerary[] = [
1519 1545 'day' => (int) $day->day_number,
1546 + /* translators: %d: itinerary day number. */
1520 1547 'day_title' => $day->title ?: sprintf(__('Day %d', 'yatra'), $day->day_number),
1521 1548 'day_description' => $day->description ?: '',
1522 1549 'entries' => $formatted_entries,
1523 1550 ];
@@ -1812,12 +1839,15 @@
1812 1839 $is_per_group = ($pricing_mode === 'per_group');
1813 1840 $pricing_label = '';
1814 1841 if ($is_per_group) {
1815 1842 if (!empty($price_type->min_pax) && !empty($price_type->max_pax)) {
1816 - $pricing_label = sprintf(__('per group (%d-%d pax)', 'yatra'), $price_type->min_pax, $price_type->max_pax);
1843 + /* translators: 1: minimum pax for the group price, 2: maximum pax. */
1844 + $pricing_label = sprintf(__('per group (%1$d-%2$d pax)', 'yatra'), $price_type->min_pax, $price_type->max_pax);
1817 1845 } elseif (!empty($price_type->max_pax)) {
1846 + /* translators: %d: maximum pax for the group price. */
1818 1847 $pricing_label = sprintf(__('per group (up to %d pax)', 'yatra'), $price_type->max_pax);
1819 1848 } elseif (!empty($price_type->min_pax)) {
1849 + /* translators: %d: minimum pax for the group price. */
1820 1850 $pricing_label = sprintf(__('per group (%d+ pax)', 'yatra'), $price_type->min_pax);
1821 1851 } else {
1822 1852 $pricing_label = __('per group', 'yatra');
1823 1853 }
@@ -1846,12 +1876,15 @@
1846 1876 $age_min = $price_type->age_min ?? null;
1847 1877 $age_max = $price_type->age_max ?? null;
1848 1878 if ($age_min !== null || $age_max !== null) {
1849 1879 if ($age_min !== null && $age_max !== null) {
1850 - $age_info = sprintf(__('(Age %d-%d)', 'yatra'), $age_min, $age_max);
1880 + /* translators: 1: minimum age, 2: maximum age. */
1881 + $age_info = sprintf(__('(Age %1$d-%2$d)', 'yatra'), $age_min, $age_max);
1851 1882 } elseif ($age_min !== null) {
1883 + /* translators: %d: minimum age. */
1852 1884 $age_info = sprintf(__('(Age %d+)', 'yatra'), $age_min);
1853 1885 } else {
1886 + /* translators: %d: maximum age. */
1854 1887 $age_info = sprintf(__('(Up to age %d)', 'yatra'), $age_max);
1855 1888 }
1856 1889 }
1857 1890
@@ -1864,8 +1897,16 @@
1864 1897
1865 1898 $input_id = 'traveler_' . $price_type->category_id;
1866 1899 $max_travelers = is_object($trip) && method_exists($trip, 'getMaxTravelers') ? $trip->getMaxTravelers() : ($trip->max_travelers ?? 20);
1867 1900 $pt_max_qty = (int) ($price_type->max_quantity ?: $max_travelers);
1901 + // A per-group category in "block" overflow mode caps the party at
1902 + // the max group size. In "per_block" mode the party may exceed it
1903 + // (it just buys additional group blocks), so we keep the trip's
1904 + // normal cap there.
1905 + if ($is_per_group && !empty($price_type->max_pax)
1906 + && (($price_type->group_overflow ?? 'block') !== 'per_block')) {
1907 + $pt_max_qty = (int) $price_type->max_pax;
1908 + }
1868 1909 $pt_value = ($index === $default_index) ? 1 : 0;
1869 1910
1870 1911 $traveler_rows[] = [
1871 1912 'label' => $price_type->category_label ?: __('Traveler', 'yatra'),
@@ -1874,17 +1915,21 @@
1874 1915 'row_attrs' => [
1875 1916 'data-category-id' => $price_type->category_id,
1876 1917 'data-price' => $price_type->effective_price,
1877 1918 'data-pricing-mode' => $pricing_mode,
1919 + 'data-group-overflow' => $price_type->group_overflow ?? 'block',
1920 + 'data-max-pax' => $price_type->max_pax ?? '',
1878 1921 ],
1879 1922 'minus_disabled' => ($index !== $default_index),
1880 1923 'plus_disabled' => false,
1881 1924 'minus_attrs' => [
1882 1925 'data-target' => $input_id,
1926 + /* translators: %s: traveler category label (e.g. "Adult", "Child"). */
1883 1927 'aria-label' => sprintf(__('Decrease %s', 'yatra'), $price_type->category_label),
1884 1928 ],
1885 1929 'plus_attrs' => [
1886 1930 'data-target' => $input_id,
1931 + /* translators: %s: traveler category label (e.g. "Adult", "Child"). */
1887 1932 'aria-label' => sprintf(__('Increase %s', 'yatra'), $price_type->category_label),
1888 1933 ],
1889 1934 'input_attrs' => [
1890 1935 'id' => $input_id,
@@ -1895,8 +1940,10 @@
1895 1940 'data-category' => $price_type->category_id,
1896 1941 'data-category-label' => $price_type->category_label,
1897 1942 'data-price' => $price_type->effective_price,
1898 1943 'data-pricing-mode' => $pricing_mode,
1944 + 'data-group-overflow' => $price_type->group_overflow ?? 'block',
1945 + 'data-max-pax' => $price_type->max_pax ?? '',
1899 1946 ],
1900 1947 ];
1901 1948 }
1902 1949
@@ -2029,9 +2076,10 @@
2029 2076 foreach ($normalized_price_types as $pt_index => $pt) {
2030 2077 $pt_min = isset($pt->age_min) ? (int) $pt->age_min : 0;
2031 2078 $pt_max = isset($pt->age_max) ? (int) $pt->age_max : 99;
2032 2079 $pt_label = $pt->category_label ?? $pt->label ?? __('Traveler', 'yatra');
2033 - $pt_age_text = ($pt_min > 0 || $pt_max < 99) ? sprintf(__('(Age %d-%d)', 'yatra'), $pt_min, $pt_max) : '';
2080 + /* translators: 1: minimum age, 2: maximum age. */
2081 + $pt_age_text = ($pt_min > 0 || $pt_max < 99) ? sprintf(__('(Age %1$d-%2$d)', 'yatra'), $pt_min, $pt_max) : '';
2034 2082
2035 2083 // Use initial traveler count if provided, otherwise use default
2036 2084 $pt_category_id = $pt->category_id ?? $pt_index;
2037 2085 $pt_default = isset($initial_travelers[$pt_category_id])
@@ -2070,17 +2118,26 @@
2070 2118 $pt_min_qty = 0;
2071 2119 $pt_max_qty = (int) min($seats_available, $max_travelers);
2072 2120 $pt_pricing_mode = $pt->pricing_mode ?? 'per_person';
2073 2121 $pt_is_per_group = ($pt_pricing_mode === 'per_group');
2074 -
2122 + // Cap a per-group "block" category at its max group size (still
2123 + // bounded by seats). "per_block" mode may exceed it, so skip.
2124 + if ($pt_is_per_group && !empty($pt->max_pax)
2125 + && (($pt->group_overflow ?? 'block') !== 'per_block')) {
2126 + $pt_max_qty = (int) min($pt_max_qty, (int) $pt->max_pax);
2127 + }
2128 +
2075 2129 // Build pricing label
2076 2130 $pricing_label = '';
2077 2131 if ($pt_is_per_group) {
2078 2132 if (!empty($pt->min_pax) && !empty($pt->max_pax)) {
2079 - $pricing_label = sprintf(__('per group (%d-%d pax)', 'yatra'), $pt->min_pax, $pt->max_pax);
2133 + /* translators: 1: minimum pax for the group price, 2: maximum pax. */
2134 + $pricing_label = sprintf(__('per group (%1$d-%2$d pax)', 'yatra'), $pt->min_pax, $pt->max_pax);
2080 2135 } elseif (!empty($pt->max_pax)) {
2136 + /* translators: %d: maximum pax for the group price. */
2081 2137 $pricing_label = sprintf(__('per group (up to %d pax)', 'yatra'), $pt->max_pax);
2082 2138 } elseif (!empty($pt->min_pax)) {
2139 + /* translators: %d: minimum pax for the group price. */
2083 2140 $pricing_label = sprintf(__('per group (%d+ pax)', 'yatra'), $pt->min_pax);
2084 2141 } else {
2085 2142 $pricing_label = __('per group', 'yatra');
2086 2143 }
@@ -2100,17 +2157,21 @@
2100 2157 'row_attrs' => [
2101 2158 'data-category-id' => $pt_category_id,
2102 2159 'data-price' => $pt_price,
2103 2160 'data-pricing-mode' => $pt_pricing_mode,
2161 + 'data-group-overflow' => $pt->group_overflow ?? 'block',
2162 + 'data-max-pax' => $pt->max_pax ?? '',
2104 2163 ],
2105 2164 'minus_disabled' => ($pt_default <= 0),
2106 2165 'plus_disabled' => false,
2107 2166 'minus_attrs' => [
2108 2167 'data-target' => 'traveler_' . $pt_category_id . '_' . $item_id,
2168 + /* translators: %s: traveler category label (e.g. "Adult", "Child"). */
2109 2169 'aria-label' => sprintf(__('Decrease %s', 'yatra'), $pt_label),
2110 2170 ],
2111 2171 'plus_attrs' => [
2112 2172 'data-target' => 'traveler_' . $pt_category_id . '_' . $item_id,
2173 + /* translators: %s: traveler category label (e.g. "Adult", "Child"). */
2113 2174 'aria-label' => sprintf(__('Increase %s', 'yatra'), $pt_label),
2114 2175 ],
2115 2176 'input_attrs' => [
2116 2177 'data-item' => $item_id,
@@ -2116,8 +2177,10 @@
2116 2177 'data-item' => $item_id,
2117 2178 'data-category' => $pt_category_id,
2118 2179 'data-price' => $pt_price,
2119 2180 'data-pricing-mode' => $pt_pricing_mode,
2181 + 'data-group-overflow' => $pt->group_overflow ?? 'block',
2182 + 'data-max-pax' => $pt->max_pax ?? '',
2120 2183 'value' => $pt_default,
2121 2184 'min' => $pt_min_qty,
2122 2185 'max' => $pt_max_qty,
2123 2186 ],