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 +337 -69 3.0.7 → 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).
@@ -102,8 +115,10 @@
102 115 'scheduled_payment_days' => 15, // Days until first scheduled payment
103 116 'scheduled_payment_installments' => 1, // Number of installments (if type is installments)
104 117 'scheduled_payment_interval' => 30, // Days between installments
105 118 'scheduled_payment_reminder_days' => 3, // Days before to send reminder
119 + 'balance_anchor' => 'booking', // 'booking' (BC default) | 'tour' (relative to tour date)
120 + 'balance_due_days' => 14, // When anchor=tour: balance due this many days before the tour
106 121 'allow_save_payment_methods' => false,
107 122
108 123 // Email Settings (WordPress site defaults when Yatra options are missing)
109 124 'admin_email' => $wpAdminEmail,
@@ -108,10 +123,16 @@
108 123 // Email Settings (WordPress site defaults when Yatra options are missing)
109 124 'admin_email' => $wpAdminEmail,
110 125 'from_email' => $wpAdminEmail,
111 126 'from_name' => $wpSiteName,
127 + // Blind copy of every outgoing Yatra email, for archiving/monitoring.
128 + // Empty means no copy is sent; accepts several comma-separated addresses.
129 + 'email_always_bcc' => '',
112 130 'email_template_booking' => true,
113 131 'email_template_confirmation' => true,
132 + // Separate part-payment email. Off by default so existing sites keep
133 + // sending the single payment template for every payment.
134 + 'email_template_partial_payment' => false,
114 135 'email_template_cancellation' => true,
115 136 'email_template_reminder' => true,
116 137 'email_template_admin_new_booking' => true,
117 138 'email_template_admin_payment' => true,
@@ -118,8 +139,10 @@
118 139 'email_template_admin_cancellation' => true,
119 140 'email_template_trip_consent' => true,
120 141 'email_template_customer_verification' => true,
121 142 'email_template_guest_verification' => true,
143 + 'email_template_account_email_change' => true,
144 + 'email_template_account_email_changed' => true,
122 145 'email_template_booking_completed' => true,
123 146 'email_template_booking_expired_customer' => true,
124 147 'email_template_admin_booking_expired' => true,
125 148 'email_template_scheduled_payment_reminder' => true,
@@ -192,9 +215,16 @@
192 215 'facebook_pixel' => '',
193 216 'recaptcha_enabled' => false,
194 217 'recaptcha_site_key' => '',
195 218 'recaptcha_secret_key' => '',
196 -
219 + // reCAPTCHA v3: score threshold (0.0-1.0) + per-form protection toggles.
220 + // All off by default so enabling reCAPTCHA alone changes nothing until
221 + // the operator picks which forms to protect.
222 + 'recaptcha_score_threshold' => 0.5,
223 + 'recaptcha_protect_enquiry' => false,
224 + 'recaptcha_protect_booking' => false,
225 + 'recaptcha_protect_registration' => false,
226 +
197 227 // Permalink Settings
198 228 'trip_base' => 'trip',
199 229 'destination_base' => 'destination',
200 230 'activity_base' => 'activity',
@@ -201,8 +231,13 @@
201 231 'trip_category_base' => 'trip-category',
202 232 'booking_base' => 'book',
203 233 // Wishlist (Pro) — stored in free options; active only when Pro + setting on
204 234 'enable_wishlist' => false,
235 + // Sold-out date visibility on the storefront. Default true keeps the
236 + // existing behaviour (sold-out dates stay visible, badged "sold out" and
237 + // able to drive the waitlist); owners can switch it off to hide them the
238 + // same way blocked dates are hidden.
239 + 'show_sold_out' => true,
205 240
206 241 // Search & Listing storefront UX. Defaults preserve current behaviour:
207 242 // every search field shown (true) and mobile filters expanded (false),
208 243 // so existing installs are unchanged until the owner opts in. Booleans
@@ -211,8 +246,12 @@
211 246 'search_show_destination' => true,
212 247 'search_show_activities' => true,
213 248 'search_show_duration' => true,
214 249 'search_show_budget' => true,
250 + // Date field is opt-in (default false) so updating the plugin never
251 + // changes an existing site's search bar. Operators enable it to let
252 + // customers find trips with a departure on a specific date.
253 + 'search_show_date' => false,
215 254 'collapse_filters_on_mobile' => false,
216 255
217 256 // Booking Page Settings
218 257 'use_booking_page' => false,
@@ -226,9 +265,17 @@
226 265 'seo_trip_meta_title' => '',
227 266 'seo_trip_meta_description' => '',
228 267 'seo_trip_meta_keywords' => '',
229 268 'seo_trip_meta_image' => 0,
230 -
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,
277 +
231 278 // Advanced Settings
232 279 'debug_mode' => false,
233 280 'enable_logging' => false,
234 281 'cache_enabled' => true,
@@ -275,8 +322,26 @@
275 322 'permission_callback' => [$this, 'check_permission'],
276 323 ],
277 324 ]);
278 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 +
279 344 // Get WordPress pages for booking page selection
280 345 register_rest_route($namespace, '/' . $base . '/pages', [
281 346 [
282 347 'methods' => \WP_REST_Server::READABLE,
@@ -361,8 +426,43 @@
361 426 return current_user_can('yatra_manage_settings');
362 427 }
363 428
364 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 + /**
365 465 * Get all settings
366 466 */
367 467 public function get_settings(WP_REST_Request $request)
368 468 {
@@ -368,18 +468,36 @@
368 468 {
369 469 try {
370 470 $settings = [];
371 471
372 - // Get all settings from WordPress options table with yatra_ prefix
472 + // Get all settings from WordPress options table with yatra_ prefix.
473 + // A sentinel default is essential here: get_option() returns boolean
474 + // false for a stored-false option just as it does for a missing one,
475 + // so checking `=== false` would reset every saved-off boolean back to
476 + // its default. That is exactly the "Show sold-out dates" bug — the
477 + // storefront honoured the saved value (isEnabled coerces '' -> false)
478 + // while the admin checkbox re-appeared enabled because this endpoint
479 + // handed React the default (true) instead of the saved false.
480 + $unset_sentinel = "\0__yatra_option_unset__\0";
373 481 foreach ($this->default_settings as $key => $default_value) {
374 482 $option_name = 'yatra_' . $key;
375 - $value = get_option($option_name, false);
376 -
377 - // Only use default if option doesn't exist (wasn't set by InstallerService)
378 - if ($value === false) {
483 + $value = get_option($option_name, $unset_sentinel);
484 +
485 + // Only use default when the option truly does not exist.
486 + if ($value === $unset_sentinel) {
379 487 $value = $default_value;
380 488 }
381 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 +
382 500 // Stored empty string should behave like "unset" for delivery identity (matches installer / backfill).
383 501 if (($key === 'admin_email' || $key === 'from_email') && is_string($value) && trim($value) === '') {
384 502 $wp = (string) get_option('admin_email', '');
385 503 $value = $wp !== '' ? $wp : $value;
@@ -397,9 +515,18 @@
397 515 // Ensure arrays are returned as arrays (not objects)
398 516 if (is_array($default_value) && !is_array($value)) {
399 517 $value = [];
400 518 }
401 -
519 +
520 + // Boolean settings must round-trip to the admin as real booleans.
521 + // update_option() stores false as '' and the object cache can
522 + // return boolean false, so without this a disabled toggle would
523 + // reach React as '' / false and the checkbox (checked unless the
524 + // value is strictly !== false) would render enabled again.
525 + if (is_bool($default_value)) {
526 + $value = filter_var($value, FILTER_VALIDATE_BOOLEAN);
527 + }
528 +
402 529 $settings[$key] = $value;
403 530 }
404 531
405 532 // Special handling for booking_form_config - always use getBookingFormConfig which handles locked fields
@@ -424,8 +551,10 @@
424 551 'scheduled_payment_days',
425 552 'scheduled_payment_installments',
426 553 'scheduled_payment_interval',
427 554 'scheduled_payment_reminder_days',
555 + 'balance_anchor',
556 + 'balance_due_days',
428 557 ] as $sk
429 558 ) {
430 559 if (array_key_exists($sk, $this->default_settings)) {
431 560 $settings[$sk] = \Yatra\Services\SettingsService::get(
@@ -484,8 +613,10 @@
484 613 'scheduled_payment_days',
485 614 'scheduled_payment_installments',
486 615 'scheduled_payment_interval',
487 616 'scheduled_payment_reminder_days',
617 + 'balance_anchor',
618 + 'balance_due_days',
488 619 ];
489 620
490 621 // Collect flexible payment settings to delegate to Pro
491 622 $flexible_payment_settings = [];
@@ -696,8 +827,19 @@
696 827 if ($filtered_value !== null) {
697 828 return $filtered_value;
698 829 }
699 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 +
700 842 // Handle null values - use default
701 843 if ($value === null) {
702 844 return $default;
703 845 }
@@ -746,8 +888,14 @@
746 888 // Validate ranges for specific fields
747 889 if ($key === 'booking_expiry_hours' && $int_value < 0) {
748 890 return null;
749 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 + }
750 898 if ($key === 'partial_payment_percentage' && ($int_value < 0 || $int_value > 100)) {
751 899 return null;
752 900 }
753 901 if ($key === 'deposit_percentage' && ($int_value < 0 || $int_value > 100)) {
@@ -830,8 +978,18 @@
830 978 return \Yatra\Utils\FrontendThemeCss::sanitizeContainerMaxWidthSetting(
831 979 is_string($value) ? $value : ''
832 980 );
833 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 + }
834 992 if (is_string($key) && strpos($key, 'email_tpl_') === 0 && substr($key, -5) === '_body') {
835 993 return wp_kses_post((string) $value);
836 994 }
837 995 if (is_string($key) && strpos($key, 'email_tpl_') === 0 && substr($key, -8) === '_subject') {
@@ -847,15 +1005,8 @@
847 1005 return $this->sanitize_gateway_configs($value);
848 1006 }
849 1007 return [];
850 1008 }
851 - if ($key === 'booking_form_config') {
852 - // Handle nested array structure for booking form config
853 - if (is_array($value)) {
854 - return $this->sanitize_booking_form_config($value);
855 - }
856 - return [];
857 - }
858 1009 if ($key === 'tax_rates') {
859 1010 // Handle nested array structure for tax rates
860 1011 if (is_array($value)) {
861 1012 return $this->sanitize_tax_rates($value);
@@ -1081,77 +1232,194 @@
1081 1232 private function sanitize_booking_form_config(array $config): array
1082 1233 {
1083 1234 $sanitized = [];
1084 1235 $allowed_form_types = ['contact_form', 'emergency_contact_form', 'traveler_form'];
1085 - $allowed_field_types = ['text', 'email', 'tel', 'date', 'select', 'country', 'textarea', 'checkbox', 'number', 'text_block'];
1086 - $allowed_widths = ['full', 'half', 'third'];
1087 -
1236 +
1088 1237 foreach ($config as $form_type => $form_config) {
1089 1238 if (!in_array($form_type, $allowed_form_types, true)) {
1090 1239 continue;
1091 1240 }
1092 -
1241 +
1093 1242 $sanitized[$form_type] = [
1094 1243 'title' => isset($form_config['title']) ? sanitize_text_field($form_config['title']) : '',
1095 1244 'description' => isset($form_config['description']) ? sanitize_text_field($form_config['description']) : '',
1096 1245 'enabled' => isset($form_config['enabled']) ? (bool) $form_config['enabled'] : true,
1097 - 'fields' => [],
1246 + 'fields' => $this->sanitize_booking_form_fields($form_config['fields'] ?? null, $form_type),
1098 1247 ];
1099 -
1100 - if (!empty($form_config['fields']) && is_array($form_config['fields'])) {
1101 - foreach ($form_config['fields'] as $field) {
1102 - if (!is_array($field) || empty($field['id'])) {
1103 - 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 + ];
1104 1326 }
1105 -
1106 - $sanitized_field = [
1107 - 'id' => sanitize_key($field['id']),
1108 - 'type' => in_array($field['type'] ?? 'text', $allowed_field_types, true) ? $field['type'] : 'text',
1109 - 'label' => isset($field['label']) ? sanitize_text_field($field['label']) : '',
1110 - 'placeholder' => isset($field['placeholder']) ? sanitize_text_field($field['placeholder']) : '',
1111 - 'required' => isset($field['required']) ? (bool) $field['required'] : false,
1112 - 'enabled' => isset($field['enabled']) ? (bool) $field['enabled'] : true,
1113 - 'order' => isset($field['order']) ? (int) $field['order'] : 0,
1114 - 'width' => in_array($field['width'] ?? 'full', $allowed_widths, true) ? $field['width'] : 'full',
1115 - 'locked' => isset($field['locked']) ? (bool) $field['locked'] : false,
1116 - ];
1117 -
1118 - // Handle optional section
1119 - if (!empty($field['section'])) {
1120 - $sanitized_field['section'] = sanitize_key($field['section']);
1121 - }
1122 -
1123 - // Handle options for select fields
1124 - if ($sanitized_field['type'] === 'select' && !empty($field['options']) && is_array($field['options'])) {
1125 - $sanitized_field['options'] = [];
1126 - foreach ($field['options'] as $option) {
1127 - if (is_array($option) && isset($option['value'])) {
1128 - $sanitized_field['options'][] = [
1129 - 'value' => sanitize_key($option['value']),
1130 - 'label' => isset($option['label']) ? sanitize_text_field($option['label']) : $option['value'],
1131 - ];
1132 - }
1133 - }
1134 - }
1327 + }
1328 + }
1135 1329
1136 - // A text block is display-only content placed between fields:
1137 - // keep its (safe-HTML) content, and it can never be required.
1138 - if ($sanitized_field['type'] === 'text_block') {
1139 - $sanitized_field['content'] = isset($field['content']) ? wp_kses_post($field['content']) : '';
1140 - $sanitized_field['required'] = false;
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 + }
1336 +
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 + }
1348 +
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;
1141 1390 }
1142 -
1143 - $sanitized[$form_type]['fields'][] = $sanitized_field;
1391 + )));
1392 + if ($ids !== []) {
1393 + $targets[$selector] = $ids;
1144 1394 }
1145 -
1146 - // Sort fields by order
1147 - usort($sanitized[$form_type]['fields'], function($a, $b) {
1148 - return ($a['order'] ?? 0) - ($b['order'] ?? 0);
1149 - });
1150 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 + ];
1151 1419 }
1152 -
1153 - return apply_filters('yatra_save_booking_form_config', $sanitized, $config);
1420 +
1421 + return $sanitized;
1154 1422 }
1155 1423
1156 1424 /**
1157 1425 * Flush rewrite rules