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/TripController.php +113 -27 3.0.4 → 3.0.16 View file →
@@ -61,23 +61,23 @@
61 61 register_rest_route($namespace, '/' . $base, [
62 62 [
63 63 'methods' => \WP_REST_Server::READABLE,
64 64 'callback' => [$this, 'get_items'],
65 - 'permission_callback' => [$this, 'check_read_permission'],
65 + 'permission_callback' => [$this, 'check_view_permission'],
66 66 ],
67 67 [
68 68 'methods' => \WP_REST_Server::CREATABLE,
69 69 'callback' => [$this, 'create_item'],
70 - 'permission_callback' => [$this, 'check_permission'],
70 + 'permission_callback' => [$this, 'check_create_permission'],
71 71 ],
72 72 ]);
73 73
74 - // Duplicate trip: POST /trips/{id}/duplicate
74 + // Duplicate is a create — produces a new trip row.
75 75 register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)/duplicate', [
76 76 [
77 77 'methods' => \WP_REST_Server::CREATABLE,
78 78 'callback' => [$this, 'duplicate_item'],
79 - 'permission_callback' => [$this, 'check_permission'],
79 + 'permission_callback' => [$this, 'check_create_permission'],
80 80 ],
81 81 ]);
82 82
83 83 register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)', [
@@ -83,46 +83,54 @@
83 83 register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)', [
84 84 [
85 85 'methods' => \WP_REST_Server::READABLE,
86 86 'callback' => [$this, 'get_item'],
87 - 'permission_callback' => [$this, 'check_read_permission'],
87 + 'permission_callback' => [$this, 'check_view_permission'],
88 88 ],
89 89 [
90 + // EDITABLE covers both content edits AND publish/unpublish
91 + // state changes (the React form sends both via PUT).
92 + // We accept either edit OR publish cap — handlers should
93 + // refuse to change `status` when the user holds only the
94 + // edit cap, but the route gate lets both through.
90 95 'methods' => \WP_REST_Server::EDITABLE,
91 96 'callback' => [$this, 'update_item'],
92 - 'permission_callback' => [$this, 'check_permission'],
97 + 'permission_callback' => [$this, 'check_edit_or_publish_permission'],
93 98 ],
94 99 [
100 + // Soft-delete (trash) → edit cap. Trash is reversible
101 + // and is the day-to-day "remove from catalogue" action.
95 102 'methods' => \WP_REST_Server::DELETABLE,
96 103 'callback' => [$this, 'delete_item'],
97 - 'permission_callback' => [$this, 'check_permission'],
104 + 'permission_callback' => [$this, 'check_edit_permission'],
98 105 ],
99 106 ]);
100 107
101 - // Permanent delete endpoint
108 + // Permanent delete — bypasses trash. High-sensitivity action,
109 + // gated on the dedicated delete cap.
102 110 register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)/permanent-delete', [
103 111 [
104 112 'methods' => \WP_REST_Server::DELETABLE,
105 113 'callback' => [$this, 'permanent_delete_item'],
106 - 'permission_callback' => [$this, 'check_permission'],
114 + 'permission_callback' => [$this, 'check_delete_permission'],
107 115 ],
108 116 ]);
109 117
110 - // Search endpoint
118 + // Search endpoint — view cap.
111 119 register_rest_route($namespace, '/' . $base . '/search', [
112 120 [
113 121 'methods' => \WP_REST_Server::READABLE,
114 122 'callback' => [$this, 'search_items'],
115 - 'permission_callback' => [$this, 'check_read_permission'],
123 + 'permission_callback' => [$this, 'check_view_permission'],
116 124 ],
117 125 ]);
118 126
119 - // Revisions endpoints
127 + // Revisions list — view cap.
120 128 register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)/revisions', [
121 129 [
122 130 'methods' => \WP_REST_Server::READABLE,
123 131 'callback' => [$this, 'get_revisions'],
124 - 'permission_callback' => [$this, 'check_read_permission'],
132 + 'permission_callback' => [$this, 'check_view_permission'],
125 133 ],
126 134 ]);
127 135
128 136 register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)/revisions/(?P<revision_id>[\d]+)', [
@@ -128,14 +136,15 @@
128 136 register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)/revisions/(?P<revision_id>[\d]+)', [
129 137 [
130 138 'methods' => \WP_REST_Server::READABLE,
131 139 'callback' => [$this, 'get_revision'],
132 - 'permission_callback' => [$this, 'check_read_permission'],
140 + 'permission_callback' => [$this, 'check_view_permission'],
133 141 ],
134 142 [
143 + // Restoring a revision overwrites the live trip → edit cap.
135 144 'methods' => \WP_REST_Server::EDITABLE,
136 145 'callback' => [$this, 'restore_revision'],
137 - 'permission_callback' => [$this, 'check_permission'],
146 + 'permission_callback' => [$this, 'check_edit_permission'],
138 147 ],
139 148 ]);
140 149
141 150 // Availability template endpoint (public, no auth required)
@@ -165,34 +174,35 @@
165 174 'permission_callback' => '__return_true', // Public endpoint
166 175 ],
167 176 ]);
168 177
169 - // Status statistics for admin views
178 + // Status statistics for admin views — view cap.
170 179 register_rest_route($namespace, '/' . $base . '/stats', [
171 180 [
172 181 'methods' => \WP_REST_Server::READABLE,
173 182 'callback' => [$this, 'getStats'],
174 - 'permission_callback' => [$this, 'check_permission'],
183 + 'permission_callback' => [$this, 'check_view_permission'],
175 184 ],
176 185 ]);
177 186
178 - // Trip attributes endpoints (admin only — never expose unauthenticated read/write)
187 + // Test endpoint — view cap (read-only diagnostic).
179 188 register_rest_route($namespace, '/' . $base . '/test', [
180 189 'methods' => \WP_REST_Server::READABLE,
181 190 'callback' => [$this, 'test_endpoint'],
182 - 'permission_callback' => [$this, 'check_permission'],
191 + 'permission_callback' => [$this, 'check_view_permission'],
183 192 ]);
184 193
194 + // Trip-attribute assignments — trip-taxonomy edits go here.
185 195 register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)/attributes', [
186 196 [
187 197 'methods' => \WP_REST_Server::READABLE,
188 198 'callback' => [$this, 'get_trip_attributes'],
189 - 'permission_callback' => [$this, 'check_read_permission'],
199 + 'permission_callback' => [$this, 'check_view_permission'],
190 200 ],
191 201 [
192 202 'methods' => \WP_REST_Server::CREATABLE,
193 203 'callback' => [$this, 'update_trip_attributes'],
194 - 'permission_callback' => [$this, 'check_permission'],
204 + 'permission_callback' => [$this, 'check_taxonomy_permission'],
195 205 ],
196 206 ]);
197 207
198 208 register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)/attributes/(?P<attribute_id>[\d]+)', [
@@ -198,14 +208,58 @@
198 208 register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)/attributes/(?P<attribute_id>[\d]+)', [
199 209 [
200 210 'methods' => \WP_REST_Server::DELETABLE,
201 211 'callback' => [$this, 'delete_trip_attribute'],
202 - 'permission_callback' => [$this, 'check_permission'],
212 + 'permission_callback' => [$this, 'check_taxonomy_permission'],
203 213 ],
204 214 ]);
205 215 }
206 216
207 217 /**
218 + * Granular cap checks for every Trip endpoint. Overrides the
219 + * BaseController defaults (which gate everything on `manage_options`
220 + * and locked out every yatra_* role from the trips REST surface).
221 + * WP admins pass via the Team module's admin-fallback filter.
222 + */
223 + public function check_view_permission(?WP_REST_Request $request = null): bool
224 + {
225 + return current_user_can('yatra_view_trips');
226 + }
227 +
228 + public function check_create_permission(?WP_REST_Request $request = null): bool
229 + {
230 + return current_user_can('yatra_create_trips');
231 + }
232 +
233 + public function check_edit_permission(?WP_REST_Request $request = null): bool
234 + {
235 + return current_user_can('yatra_edit_trips');
236 + }
237 +
238 + /**
239 + * EDITABLE / PUT routes that may carry either a content edit or a
240 + * status change pass when the caller holds EITHER cap. The actual
241 + * handler should refuse to change `status` when only `edit` is
242 + * held — that's a future hardening, but the route gate already
243 + * keeps non-trip-staff out.
244 + */
245 + public function check_edit_or_publish_permission(?WP_REST_Request $request = null): bool
246 + {
247 + return current_user_can('yatra_edit_trips')
248 + || current_user_can('yatra_publish_trips');
249 + }
250 +
251 + public function check_delete_permission(?WP_REST_Request $request = null): bool
252 + {
253 + return current_user_can('yatra_delete_trips');
254 + }
255 +
256 + public function check_taxonomy_permission(?WP_REST_Request $request = null): bool
257 + {
258 + return current_user_can('yatra_manage_trip_taxonomies');
259 + }
260 +
261 + /**
208 262 * Get statistics for admin trip views (status counts)
209 263 */
210 264 public function getStats(WP_REST_Request $request)
211 265 {
@@ -888,10 +942,15 @@
888 942 */
889 943 public function restore_revision(WP_REST_Request $request)
890 944 {
891 945 try {
892 - // Check permissions
893 - if (!current_user_can('yatra_edit_trips')) {
946 + // Check permissions — admin fallback ensures site owners
947 + // always pass even when the Team module isn't active and
948 + // the yatra_edit_trips cap isn't on the admin role.
949 + if (
950 + !current_user_can('manage_options')
951 + && !current_user_can('yatra_edit_trips')
952 + ) {
894 953 return $this->error_response(__('You do not have permission to restore revisions', 'yatra'), 403);
895 954 }
896 955
897 956 $id = (int) $request->get_param('id');
@@ -1456,13 +1515,15 @@
1456 1515
1457 1516 // Fetch availability dates using centralized resolution service
1458 1517 $resolutionService = new \Yatra\Services\AvailabilityResolutionService();
1459 1518
1460 - // Always show all dates from today onwards (selected_date is only for highlighting)
1519 + // Always show all dates from today onwards (selected_date is only for
1520 + // highlighting), up to the configurable booking horizon (Settings →
1521 + // Booking; 12 months unless changed — the previous hard-coded value).
1461 1522 $fromDate = date('Y-m-d');
1462 - $toDate = date('Y-m-d', strtotime('+12 months'));
1523 + $toDate = yatra_get_availability_horizon_date($fromDate);
1463 1524
1464 - $availability_dates = $resolutionService->getAllAvailabilityDates($id, $fromDate, $toDate);
1525 + $availability_dates = $resolutionService->getAllAvailabilityDates($id, $fromDate, $toDate, \Yatra\Services\SettingsService::isEnabled('show_sold_out'));
1465 1526
1466 1527 // Determine if this is a day trip
1467 1528 $is_single_day = ($trip->duration_days ?? 1) <= 1;
1468 1529
@@ -1534,8 +1595,11 @@
1534 1595 'discounted_price' => isset($trip->discounted_price) ? (float) $trip->discounted_price : 0,
1535 1596 'sale_price' => isset($trip->sale_price) ? (float) $trip->sale_price : 0,
1536 1597 'currency' => SettingsService::getCurrency(),
1537 1598 'duration_days' => isset($trip->duration_days) ? (int) $trip->duration_days : 1,
1599 + // Hour-based day tours show "8 Hours" instead of "1 Day" on the
1600 + // availability cards. 0 for every existing (day-based) trip.
1601 + 'duration_hours' => isset($trip->duration_hours) ? (int) $trip->duration_hours : 0,
1538 1602 'max_travelers' => isset($trip->max_travelers) ? (int) $trip->max_travelers : 20,
1539 1603 'min_travelers' => isset($trip->min_travelers) ? (int) $trip->min_travelers : 1,
1540 1604 'pricing_type' => $trip->pricing_type ?? 'regular',
1541 1605 'price_types' => $trip->price_types ?? [], // Include price_types for traveler-based pricing
@@ -1776,8 +1840,9 @@
1776 1840 'age_min' => isset($meta['age_min']) ? (int) $meta['age_min'] : null,
1777 1841 'age_max' => isset($meta['age_max']) ? (int) $meta['age_max'] : null,
1778 1842 'min_pax' => isset($meta['min_pax']) ? (int) $meta['min_pax'] : null,
1779 1843 'max_pax' => isset($meta['max_pax']) ? (int) $meta['max_pax'] : null,
1844 + 'group_overflow' => isset($meta['group_overflow']) && in_array($meta['group_overflow'], ['block', 'per_block'], true) ? $meta['group_overflow'] : 'block',
1780 1845 ];
1781 1846 }
1782 1847 }
1783 1848
@@ -1827,8 +1892,9 @@
1827 1892 if (!isset($pt['age_min'])) $pt['age_min'] = $meta['age_min'];
1828 1893 if (!isset($pt['age_max'])) $pt['age_max'] = $meta['age_max'];
1829 1894 if (!isset($pt['min_pax'])) $pt['min_pax'] = $meta['min_pax'];
1830 1895 if (!isset($pt['max_pax'])) $pt['max_pax'] = $meta['max_pax'];
1896 + $pt['group_overflow'] = $meta['group_overflow'] ?? 'block';
1831 1897 }
1832 1898
1833 1899 // Payable amount (honors price / sale_price / discounted_price like TripPricingService)
1834 1900 if (!isset($pt['effective_price'])) {
@@ -1974,8 +2040,22 @@
1974 2040 $from_label = __('Departure', 'yatra');
1975 2041 $to_label = __('Return', 'yatra');
1976 2042 }
1977 2043
2044 + // Per-card duration: derive from THIS card's departure→return span
2045 + // so the displayed "X Days" always matches the departure/return
2046 + // dates shown on the same card. When no custom arrival is stored,
2047 + // $return_date is departure + (duration_days - 1), so the span
2048 + // equals the trip's duration_days (no visible change). Only when an
2049 + // operator stored an arrival that disagrees with the trip default
2050 + // does this diverge — and then the customer sees a self-consistent
2051 + // card (e.g. "10 Days" over a Jun 25 → Jul 04 span) instead of a
2052 + // "9 Days" badge contradicting the dates. round() (not floor())
2053 + // absorbs any ±1h DST drift between two local-midnight timestamps.
2054 + $card_duration_days = $is_single_day
2055 + ? max(1, (int) ($trip_data->duration_days ?? 1))
2056 + : max(1, (int) round(($return_date - $departure_date) / DAY_IN_SECONDS) + 1);
2057 +
1978 2058 // Use month-based keys for filtering for both day trips and multi-day trips
1979 2059 $filter_key = strtolower(date('M-Y', $departure_date));
1980 2060
1981 2061 // Must match {@see TripPricingService::resolveCardPricing}: trip-level mode wins; do not
@@ -1995,8 +2075,9 @@
1995 2075 'to_label' => $to_label,
1996 2076 'to_date' => $to_display,
1997 2077 'to_location' => $to_location,
1998 2078 'date_display' => $date_display, // For day trips: "Saturday, 30 Nov 2025"
2079 + 'duration_days' => $card_duration_days, // Inclusive span of THIS card's dates
1999 2080 'date' => $avail->departure_date, // Raw date for dynamic pricing
2000 2081 'spots_remaining' => $seats, // For dynamic pricing
2001 2082 'seats' => $seats > 10 ? '10+' : (string) $seats,
2002 2083 'seats_available' => $seats,
@@ -2019,8 +2100,10 @@
2019 2100 'is_sold_out' => $is_sold_out,
2020 2101 // Card-specific pricing
2021 2102 'pricing_type' => $card_pricing_type,
2022 2103 'traveler_pricing' => $card_traveler_pricing,
2104 + 'price_unit' => $cardPricing['price_unit'] ?? 'per_person',
2105 + 'price_unit_label' => $cardPricing['price_unit_label'] ?? __('per person', 'yatra'),
2023 2106 'is_recurring' => !empty($avail->is_recurring),
2024 2107 'rule_id' => $avail->rule_id ?? null,
2025 2108 ] + $dp_card_fields;
2026 2109 }
@@ -2183,8 +2266,9 @@
2183 2266
2184 2267 if ($dynamic_pricing_enabled && $B > $eps && $F > $B + $eps) {
2185 2268 $p = (int) round((($F - $B) / $B) * 100);
2186 2269
2270 + /* translators: %d: dynamic pricing increase percentage. */
2187 2271 return $p > 0 ? sprintf(__('+%d%%', 'yatra'), $p) : '';
2188 2272 }
2189 2273
2190 2274 if ($O > $eps && $F < $O - $eps) {
@@ -2189,8 +2273,9 @@
2189 2273
2190 2274 if ($O > $eps && $F < $O - $eps) {
2191 2275 $p = (int) round((($O - $F) / $O) * 100);
2192 2276
2277 + /* translators: %d: discount percentage. */
2193 2278 return $p > 0 ? sprintf(__('%d%% OFF', 'yatra'), $p) : '';
2194 2279 }
2195 2280
2196 2281 if ($O <= $eps && $B > $eps && $F < $B - $eps) {
@@ -2195,8 +2280,9 @@
2195 2280
2196 2281 if ($O <= $eps && $B > $eps && $F < $B - $eps) {
2197 2282 $p = (int) round((($B - $F) / $B) * 100);
2198 2283
2284 + /* translators: %d: discount percentage. */
2199 2285 return $p > 0 ? sprintf(__('%d%% OFF', 'yatra'), $p) : '';
2200 2286 }
2201 2287
2202 2288 return '';