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/SettingsController.php +279 -81 3.0.14.2 → 3.0.16 View file →
@@ -46,12 +46,24 @@
46 46 'date_format' => 'Y-m-d',
47 47 'time_format' => 'H:i',
48 48 'frontend_primary_color' => '#3b82f6',
49 49 'frontend_container_max_width' => '',
50 + // Trip listing card density. 'standard' = the current comfortable card;
51 + // 'compact_mobile' = compact card on phones/tablets only (desktop grid
52 + // unchanged); 'compact_all' = compact card at every screen size.
53 + 'frontend_listing_card_layout' => 'standard',
50 54
51 55 // Booking Settings
52 56 'booking_confirmation' => true,
57 + // Legacy boolean, kept for backward compatibility. Superseded by
58 + // 'auto_confirm_mode' below; the mode is authoritative once stored.
53 59 'auto_confirm_bookings' => false,
60 + // Auto-confirm mode: 'none' (never), 'online' (only successful online
61 + // gateway payments), or 'all' (confirm every booking at checkout).
62 + // Default 'online' (payment complete => confirmed). Existing sites with
63 + // no stored mode resolve on the fly via yatra_get_auto_confirm_mode()
64 + // (legacy true->all, false->online), preserving their prior behaviour.
65 + 'auto_confirm_mode' => 'online',
54 66 'auto_confirm_pay_later' => true,
55 67 'require_login' => false,
56 68 'allow_guest_checkout' => true,
57 69 // cancellation_policy / cancellation_days / refund_policy were
@@ -65,8 +77,9 @@
65 77 // accepts them, and the email template skips the cancellation
66 78 // paragraph when the global setting is absent.
67 79 'booking_expiry_hours' => 24,
68 80 'booking_reminder_days' => 3,
81 + 'availability_horizon_months' => 12,
69 82 'allow_waitlist' => true,
70 83 'waitlist_auto_confirm' => false,
71 84 // Pro: render available departure dates as a <select> instead of a
72 85 // flatpickr calendar on the single-trip sidebar (desktop + mobile).
@@ -253,8 +266,15 @@
253 266 'seo_trip_meta_description' => '',
254 267 'seo_trip_meta_keywords' => '',
255 268 'seo_trip_meta_image' => 0,
256 269 'enable_sitemap' => true,
270 + // Which Yatra content types appear in /yatra-sitemap.xml. Defaults to
271 + // every type, so a site that never touches this keeps today's sitemap.
272 + 'sitemap_types' => ['archive', 'trip', 'destination', 'activity', 'category'],
273 + // Opt-in, and deliberately separate from the list above: dropping a type
274 + // from the sitemap is housekeeping, while noindex de-indexes pages that
275 + // may currently rank. That should never happen as a side effect.
276 + 'sitemap_noindex_excluded' => false,
257 277
258 278 // Advanced Settings
259 279 'debug_mode' => false,
260 280 'enable_logging' => false,
@@ -302,8 +322,26 @@
302 322 'permission_callback' => [$this, 'check_permission'],
303 323 ],
304 324 ]);
305 325
326 + // Booking form config, optionally resolved for one trip (Pro form
327 + // conditions). Readable by anyone who can view bookings, so the
328 + // booking detail screen can label the fields a trip actually asked.
329 + register_rest_route($namespace, '/' . $base . '/booking-form', [
330 + [
331 + 'methods' => \WP_REST_Server::READABLE,
332 + 'callback' => [$this, 'get_booking_form_config'],
333 + 'permission_callback' => [$this, 'check_booking_form_permission'],
334 + 'args' => [
335 + 'trip_id' => [
336 + 'type' => 'integer',
337 + 'required' => false,
338 + 'sanitize_callback' => 'absint',
339 + ],
340 + ],
341 + ],
342 + ]);
343 +
306 344 // Get WordPress pages for booking page selection
307 345 register_rest_route($namespace, '/' . $base . '/pages', [
308 346 [
309 347 'methods' => \WP_REST_Server::READABLE,
@@ -388,8 +426,43 @@
388 426 return current_user_can('yatra_manage_settings');
389 427 }
390 428
391 429 /**
430 + * The booking form config is needed to label booking data, so it is
431 + * readable by booking staff, not only settings managers.
432 + */
433 + public function check_booking_form_permission(?WP_REST_Request $request = null): bool
434 + {
435 + if (!is_user_logged_in()) {
436 + return false;
437 + }
438 + return current_user_can('yatra_manage_settings')
439 + || current_user_can('yatra_view_bookings')
440 + || current_user_can('yatra_edit_bookings');
441 + }
442 +
443 + /**
444 + * GET /settings/booking-form[?trip_id=N]
445 + *
446 + * Without trip_id: the full config exactly as the Settings screen sees it.
447 + * With trip_id: the config as that trip's checkout renders it — Pro form
448 + * conditions resolved (no Pro / no conditions → identical to the global).
449 + */
450 + public function get_booking_form_config(WP_REST_Request $request)
451 + {
452 + try {
453 + $trip_id = (int) $request->get_param('trip_id');
454 +
455 + return $this->success_response([
456 + 'booking_form_config' => \Yatra\Services\SettingsService::getBookingFormConfig($trip_id > 0 ? $trip_id : null),
457 + 'trip_id' => $trip_id > 0 ? $trip_id : null,
458 + ]);
459 + } catch (\Exception $e) {
460 + return $this->error_response($e->getMessage(), 500);
461 + }
462 + }
463 +
464 + /**
392 465 * Get all settings
393 466 */
394 467 public function get_settings(WP_REST_Request $request)
395 468 {
@@ -413,8 +486,18 @@
413 486 if ($value === $unset_sentinel) {
414 487 $value = $default_value;
415 488 }
416 489
490 + // Auto-Confirm mode has no stored default — it is resolved on
491 + // the fly. Return the effective mode so the admin shows the
492 + // site's real behaviour: a stored choice if the operator made
493 + // one, otherwise derived from the legacy boolean
494 + // (true -> 'all', false -> 'online'). Prevents an existing
495 + // "confirm all" site from displaying (and re-saving) as 'online'.
496 + if ($key === 'auto_confirm_mode' && function_exists('yatra_get_auto_confirm_mode')) {
497 + $value = yatra_get_auto_confirm_mode();
498 + }
499 +
417 500 // Stored empty string should behave like "unset" for delivery identity (matches installer / backfill).
418 501 if (($key === 'admin_email' || $key === 'from_email') && is_string($value) && trim($value) === '') {
419 502 $wp = (string) get_option('admin_email', '');
420 503 $value = $wp !== '' ? $wp : $value;
@@ -744,8 +827,19 @@
744 827 if ($filtered_value !== null) {
745 828 return $filtered_value;
746 829 }
747 830
831 + // The booking-form config has its own structured sanitiser (field type
832 + // and width whitelists, locked core fields, text-block content, per-trip
833 + // conditions). It must run BEFORE the generic
834 + // is_array($default) branch below: that branch only text-sanitises
835 + // values and was catching this key first — because its default is [] —
836 + // so the structured sanitiser further down was never reached and any
837 + // shape at all was stored.
838 + if ($key === 'booking_form_config') {
839 + return is_array($value) ? $this->sanitize_booking_form_config($value) : [];
840 + }
841 +
748 842 // Handle null values - use default
749 843 if ($value === null) {
750 844 return $default;
751 845 }
@@ -794,8 +888,14 @@
794 888 // Validate ranges for specific fields
795 889 if ($key === 'booking_expiry_hours' && $int_value < 0) {
796 890 return null;
797 891 }
892 + // Storefront booking horizon: 1–36 months. Out of range is rejected
893 + // (not clamped) so a bad write can never blank the calendar — the
894 + // previously stored value, or the 12-month default, stays in force.
895 + if ($key === 'availability_horizon_months' && ($int_value < 1 || $int_value > 36)) {
896 + return null;
897 + }
798 898 if ($key === 'partial_payment_percentage' && ($int_value < 0 || $int_value > 100)) {
799 899 return null;
800 900 }
801 901 if ($key === 'deposit_percentage' && ($int_value < 0 || $int_value > 100)) {
@@ -878,8 +978,18 @@
878 978 return \Yatra\Utils\FrontendThemeCss::sanitizeContainerMaxWidthSetting(
879 979 is_string($value) ? $value : ''
880 980 );
881 981 }
982 + if ($key === 'frontend_listing_card_layout') {
983 + $allowed = ['standard', 'compact_mobile', 'compact_all'];
984 + $v = is_string($value) ? strtolower(trim($value)) : '';
985 + return in_array($v, $allowed, true) ? $v : 'standard';
986 + }
987 + if ($key === 'auto_confirm_mode') {
988 + $allowed = ['none', 'online', 'all'];
989 + $v = is_string($value) ? strtolower(trim($value)) : '';
990 + return in_array($v, $allowed, true) ? $v : 'online';
991 + }
882 992 if (is_string($key) && strpos($key, 'email_tpl_') === 0 && substr($key, -5) === '_body') {
883 993 return wp_kses_post((string) $value);
884 994 }
885 995 if (is_string($key) && strpos($key, 'email_tpl_') === 0 && substr($key, -8) === '_subject') {
@@ -895,15 +1005,8 @@
895 1005 return $this->sanitize_gateway_configs($value);
896 1006 }
897 1007 return [];
898 1008 }
899 - if ($key === 'booking_form_config') {
900 - // Handle nested array structure for booking form config
901 - if (is_array($value)) {
902 - return $this->sanitize_booking_form_config($value);
903 - }
904 - return [];
905 - }
906 1009 if ($key === 'tax_rates') {
907 1010 // Handle nested array structure for tax rates
908 1011 if (is_array($value)) {
909 1012 return $this->sanitize_tax_rates($value);
@@ -1129,99 +1232,194 @@
1129 1232 private function sanitize_booking_form_config(array $config): array
1130 1233 {
1131 1234 $sanitized = [];
1132 1235 $allowed_form_types = ['contact_form', 'emergency_contact_form', 'traveler_form'];
1133 - $allowed_field_types = ['text', 'email', 'tel', 'date', 'select', 'country', 'textarea', 'checkbox', 'number', 'text_block'];
1134 - $allowed_widths = ['full', 'half', 'third'];
1135 -
1236 +
1136 1237 foreach ($config as $form_type => $form_config) {
1137 1238 if (!in_array($form_type, $allowed_form_types, true)) {
1138 1239 continue;
1139 1240 }
1140 -
1241 +
1141 1242 $sanitized[$form_type] = [
1142 1243 'title' => isset($form_config['title']) ? sanitize_text_field($form_config['title']) : '',
1143 1244 'description' => isset($form_config['description']) ? sanitize_text_field($form_config['description']) : '',
1144 1245 'enabled' => isset($form_config['enabled']) ? (bool) $form_config['enabled'] : true,
1145 - 'fields' => [],
1246 + 'fields' => $this->sanitize_booking_form_fields($form_config['fields'] ?? null, $form_type),
1146 1247 ];
1147 -
1148 - if (!empty($form_config['fields']) && is_array($form_config['fields'])) {
1149 - foreach ($form_config['fields'] as $field) {
1150 - if (!is_array($field) || empty($field['id'])) {
1151 - continue;
1248 +
1249 + // Per-trip form conditions (Pro Dynamic Form Field): each condition
1250 + // is a complete alternative version of this section — its own
1251 + // title, description and field list — used on the trips it names.
1252 + // Only persisted when there is at least one, so configs saved
1253 + // without the feature stay byte-identical.
1254 + $conditions = $this->sanitize_booking_form_conditions($form_config['conditions'] ?? null, $form_type);
1255 + if ($conditions !== []) {
1256 + $sanitized[$form_type]['conditions'] = $conditions;
1257 + }
1258 + }
1259 +
1260 + return apply_filters('yatra_save_booking_form_config', $sanitized, $config);
1261 + }
1262 +
1263 + /**
1264 + * Sanitise one section's field list (global fields or a condition's fields).
1265 + *
1266 + * @param mixed $fields
1267 + * @return array<int, array<string, mixed>>
1268 + */
1269 + private function sanitize_booking_form_fields($fields, string $form_type): array
1270 + {
1271 + $allowed_field_types = ['text', 'email', 'tel', 'date', 'select', 'country', 'textarea', 'checkbox', 'number', 'text_block'];
1272 + $allowed_widths = ['full', 'half', 'third'];
1273 + $sanitized = [];
1274 +
1275 + if (empty($fields) || !is_array($fields)) {
1276 + return $sanitized;
1277 + }
1278 +
1279 + foreach ($fields as $field) {
1280 + if (!is_array($field) || empty($field['id'])) {
1281 + continue;
1282 + }
1283 +
1284 + $sanitized_field = [
1285 + 'id' => sanitize_key($field['id']),
1286 + 'type' => in_array($field['type'] ?? 'text', $allowed_field_types, true) ? $field['type'] : 'text',
1287 + 'label' => isset($field['label']) ? sanitize_text_field($field['label']) : '',
1288 + 'placeholder' => isset($field['placeholder']) ? sanitize_text_field($field['placeholder']) : '',
1289 + 'required' => isset($field['required']) ? (bool) $field['required'] : false,
1290 + 'enabled' => isset($field['enabled']) ? (bool) $field['enabled'] : true,
1291 + 'order' => isset($field['order']) ? (int) $field['order'] : 0,
1292 + 'width' => in_array($field['width'] ?? 'full', $allowed_widths, true) ? ($field['width'] ?? 'full') : 'full',
1293 + ];
1294 +
1295 + // Only persist `locked` when set: every reader treats a missing key
1296 + // as unlocked, and configs saved before this sanitiser ran never
1297 + // carried a `locked => false`, so they stay byte-identical.
1298 + if (!empty($field['locked'])) {
1299 + $sanitized_field['locked'] = true;
1300 + }
1301 +
1302 + // Handle optional section
1303 + if (!empty($field['section'])) {
1304 + $sanitized_field['section'] = sanitize_key($field['section']);
1305 + }
1306 +
1307 + // Per-traveler targeting — Traveler section only. Whitelist
1308 + // the allowed values; only persist the non-default "lead" so
1309 + // other sections and existing configs stay byte-identical.
1310 + if (
1311 + $form_type === 'traveler_form'
1312 + && ($field['applies_to'] ?? 'all') === 'lead'
1313 + ) {
1314 + $sanitized_field['applies_to'] = 'lead';
1315 + }
1316 +
1317 + // Handle options for select fields
1318 + if ($sanitized_field['type'] === 'select' && !empty($field['options']) && is_array($field['options'])) {
1319 + $sanitized_field['options'] = [];
1320 + foreach ($field['options'] as $option) {
1321 + if (is_array($option) && isset($option['value'])) {
1322 + $sanitized_field['options'][] = [
1323 + 'value' => sanitize_key($option['value']),
1324 + 'label' => isset($option['label']) ? sanitize_text_field($option['label']) : $option['value'],
1325 + ];
1152 1326 }
1153 -
1154 - $sanitized_field = [
1155 - 'id' => sanitize_key($field['id']),
1156 - 'type' => in_array($field['type'] ?? 'text', $allowed_field_types, true) ? $field['type'] : 'text',
1157 - 'label' => isset($field['label']) ? sanitize_text_field($field['label']) : '',
1158 - 'placeholder' => isset($field['placeholder']) ? sanitize_text_field($field['placeholder']) : '',
1159 - 'required' => isset($field['required']) ? (bool) $field['required'] : false,
1160 - 'enabled' => isset($field['enabled']) ? (bool) $field['enabled'] : true,
1161 - 'order' => isset($field['order']) ? (int) $field['order'] : 0,
1162 - 'width' => in_array($field['width'] ?? 'full', $allowed_widths, true) ? ($field['width'] ?? 'full') : 'full',
1163 - 'locked' => isset($field['locked']) ? (bool) $field['locked'] : false,
1164 - ];
1165 -
1166 - // Handle optional section
1167 - if (!empty($field['section'])) {
1168 - $sanitized_field['section'] = sanitize_key($field['section']);
1169 - }
1327 + }
1328 + }
1170 1329
1171 - // Per-traveler targeting — Traveler section only. Whitelist
1172 - // the allowed values; only persist the non-default "lead" so
1173 - // other sections and existing configs stay byte-identical.
1174 - if (
1175 - $form_type === 'traveler_form'
1176 - && ($field['applies_to'] ?? 'all') === 'lead'
1177 - ) {
1178 - $sanitized_field['applies_to'] = 'lead';
1179 - }
1180 -
1181 - // Handle options for select fields
1182 - if ($sanitized_field['type'] === 'select' && !empty($field['options']) && is_array($field['options'])) {
1183 - $sanitized_field['options'] = [];
1184 - foreach ($field['options'] as $option) {
1185 - if (is_array($option) && isset($option['value'])) {
1186 - $sanitized_field['options'][] = [
1187 - 'value' => sanitize_key($option['value']),
1188 - 'label' => isset($option['label']) ? sanitize_text_field($option['label']) : $option['value'],
1189 - ];
1190 - }
1191 - }
1192 - }
1330 + // A text block is display-only content placed between fields:
1331 + // keep its (safe-HTML) content, and it can never be required.
1332 + if ($sanitized_field['type'] === 'text_block') {
1333 + $sanitized_field['content'] = isset($field['content']) ? wp_kses_post($field['content']) : '';
1334 + $sanitized_field['required'] = false;
1335 + }
1193 1336
1194 - // A text block is display-only content placed between fields:
1195 - // keep its (safe-HTML) content, and it can never be required.
1196 - if ($sanitized_field['type'] === 'text_block') {
1197 - $sanitized_field['content'] = isset($field['content']) ? wp_kses_post($field['content']) : '';
1198 - $sanitized_field['required'] = false;
1199 - }
1337 + // Phone fields: the country-code selector is ON by default.
1338 + // Only persist the non-default `false`, so existing configs
1339 + // (which never carried this key) stay byte-identical and read
1340 + // back as ON.
1341 + if (
1342 + $sanitized_field['type'] === 'tel'
1343 + && array_key_exists('show_country_code', $field)
1344 + && !$field['show_country_code']
1345 + ) {
1346 + $sanitized_field['show_country_code'] = false;
1347 + }
1200 1348
1201 - // Phone fields: the country-code selector is ON by default.
1202 - // Only persist the non-default `false`, so existing configs
1203 - // (which never carried this key) stay byte-identical and read
1204 - // back as ON.
1205 - if (
1206 - $sanitized_field['type'] === 'tel'
1207 - && array_key_exists('show_country_code', $field)
1208 - && !$field['show_country_code']
1209 - ) {
1210 - $sanitized_field['show_country_code'] = false;
1349 + $sanitized[] = $sanitized_field;
1350 + }
1351 +
1352 + // Sort fields by order
1353 + usort($sanitized, function ($a, $b) {
1354 + return ($a['order'] ?? 0) - ($b['order'] ?? 0);
1355 + });
1356 +
1357 + return $sanitized;
1358 + }
1359 +
1360 + /**
1361 + * Sanitise a section's per-trip conditions. A condition without any
1362 + * target (trip, category or trip type) can never match and is dropped.
1363 + *
1364 + * @param mixed $conditions
1365 + * @return array<int, array<string, mixed>>
1366 + */
1367 + private function sanitize_booking_form_conditions($conditions, string $form_type): array
1368 + {
1369 + if (empty($conditions) || !is_array($conditions)) {
1370 + return [];
1371 + }
1372 +
1373 + $allowed_trip_types = ['single_day', 'multi_day', 'flexible'];
1374 + $sanitized = [];
1375 + $n = 0;
1376 +
1377 + foreach ($conditions as $condition) {
1378 + if (!is_array($condition)) {
1379 + continue;
1380 + }
1381 + $n++;
1382 +
1383 + $raw_targets = is_array($condition['targets'] ?? null) ? $condition['targets'] : [];
1384 + $targets = [];
1385 + foreach (['trips', 'categories'] as $selector) {
1386 + $ids = array_values(array_unique(array_filter(
1387 + array_map('intval', is_array($raw_targets[$selector] ?? null) ? $raw_targets[$selector] : []),
1388 + static function ($id) {
1389 + return $id > 0;
1211 1390 }
1212 -
1213 - $sanitized[$form_type]['fields'][] = $sanitized_field;
1391 + )));
1392 + if ($ids !== []) {
1393 + $targets[$selector] = $ids;
1214 1394 }
1215 -
1216 - // Sort fields by order
1217 - usort($sanitized[$form_type]['fields'], function($a, $b) {
1218 - return ($a['order'] ?? 0) - ($b['order'] ?? 0);
1219 - });
1220 1395 }
1396 + $types = array_values(array_unique(array_filter(
1397 + array_map(static function ($t) {
1398 + return sanitize_key((string) $t);
1399 + }, is_array($raw_targets['trip_types'] ?? null) ? $raw_targets['trip_types'] : []),
1400 + static function ($t) use ($allowed_trip_types) {
1401 + return in_array($t, $allowed_trip_types, true);
1402 + }
1403 + )));
1404 + if ($types !== []) {
1405 + $targets['trip_types'] = $types;
1406 + }
1407 + if ($targets === []) {
1408 + continue;
1409 + }
1410 +
1411 + $id = sanitize_key((string) ($condition['id'] ?? ''));
1412 + $sanitized[] = [
1413 + 'id' => $id !== '' ? $id : 'condition_' . $n,
1414 + 'targets' => $targets,
1415 + 'title' => isset($condition['title']) ? sanitize_text_field($condition['title']) : '',
1416 + 'description' => isset($condition['description']) ? sanitize_text_field($condition['description']) : '',
1417 + 'fields' => $this->sanitize_booking_form_fields($condition['fields'] ?? null, $form_type),
1418 + ];
1221 1419 }
1222 -
1223 - return apply_filters('yatra_save_booking_form_config', $sanitized, $config);
1420 +
1421 + return $sanitized;
1224 1422 }
1225 1423
1226 1424 /**
1227 1425 * Flush rewrite rules