PluginProbe
Yatra – Travel Booking & Tour Operator Software / trunk
Yatra – Travel Booking & Tour Operator Software vtrunk
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 2.0.10 2.0.11 All 82 releases
← All changes | app/Controllers/SettingsController.php +100 -12 3.0.5trunk View file →
@@ -102,8 +102,10 @@
102 102 'scheduled_payment_days' => 15, // Days until first scheduled payment
103 103 'scheduled_payment_installments' => 1, // Number of installments (if type is installments)
104 104 'scheduled_payment_interval' => 30, // Days between installments
105 105 'scheduled_payment_reminder_days' => 3, // Days before to send reminder
106 + 'balance_anchor' => 'booking', // 'booking' (BC default) | 'tour' (relative to tour date)
107 + 'balance_due_days' => 14, // When anchor=tour: balance due this many days before the tour
106 108 'allow_save_payment_methods' => false,
107 109
108 110 // Email Settings (WordPress site defaults when Yatra options are missing)
109 111 'admin_email' => $wpAdminEmail,
@@ -108,10 +110,16 @@
108 110 // Email Settings (WordPress site defaults when Yatra options are missing)
109 111 'admin_email' => $wpAdminEmail,
110 112 'from_email' => $wpAdminEmail,
111 113 'from_name' => $wpSiteName,
114 + // Blind copy of every outgoing Yatra email, for archiving/monitoring.
115 + // Empty means no copy is sent; accepts several comma-separated addresses.
116 + 'email_always_bcc' => '',
112 117 'email_template_booking' => true,
113 118 'email_template_confirmation' => true,
119 + // Separate part-payment email. Off by default so existing sites keep
120 + // sending the single payment template for every payment.
121 + 'email_template_partial_payment' => false,
114 122 'email_template_cancellation' => true,
115 123 'email_template_reminder' => true,
116 124 'email_template_admin_new_booking' => true,
117 125 'email_template_admin_payment' => true,
@@ -118,8 +126,10 @@
118 126 'email_template_admin_cancellation' => true,
119 127 'email_template_trip_consent' => true,
120 128 'email_template_customer_verification' => true,
121 129 'email_template_guest_verification' => true,
130 + 'email_template_account_email_change' => true,
131 + 'email_template_account_email_changed' => true,
122 132 'email_template_booking_completed' => true,
123 133 'email_template_booking_expired_customer' => true,
124 134 'email_template_admin_booking_expired' => true,
125 135 'email_template_scheduled_payment_reminder' => true,
@@ -192,9 +202,16 @@
192 202 'facebook_pixel' => '',
193 203 'recaptcha_enabled' => false,
194 204 'recaptcha_site_key' => '',
195 205 'recaptcha_secret_key' => '',
196 -
206 + // reCAPTCHA v3: score threshold (0.0-1.0) + per-form protection toggles.
207 + // All off by default so enabling reCAPTCHA alone changes nothing until
208 + // the operator picks which forms to protect.
209 + 'recaptcha_score_threshold' => 0.5,
210 + 'recaptcha_protect_enquiry' => false,
211 + 'recaptcha_protect_booking' => false,
212 + 'recaptcha_protect_registration' => false,
213 +
197 214 // Permalink Settings
198 215 'trip_base' => 'trip',
199 216 'destination_base' => 'destination',
200 217 'activity_base' => 'activity',
@@ -201,9 +218,29 @@
201 218 'trip_category_base' => 'trip-category',
202 219 'booking_base' => 'book',
203 220 // Wishlist (Pro) — stored in free options; active only when Pro + setting on
204 221 'enable_wishlist' => false,
205 -
222 + // Sold-out date visibility on the storefront. Default true keeps the
223 + // existing behaviour (sold-out dates stay visible, badged "sold out" and
224 + // able to drive the waitlist); owners can switch it off to hide them the
225 + // same way blocked dates are hidden.
226 + 'show_sold_out' => true,
227 +
228 + // Search & Listing storefront UX. Defaults preserve current behaviour:
229 + // every search field shown (true) and mobile filters expanded (false),
230 + // so existing installs are unchanged until the owner opts in. Booleans
231 + // are auto-sanitized from the default type.
232 + 'search_show_keyword' => true,
233 + 'search_show_destination' => true,
234 + 'search_show_activities' => true,
235 + 'search_show_duration' => true,
236 + 'search_show_budget' => true,
237 + // Date field is opt-in (default false) so updating the plugin never
238 + // changes an existing site's search bar. Operators enable it to let
239 + // customers find trips with a departure on a specific date.
240 + 'search_show_date' => false,
241 + 'collapse_filters_on_mobile' => false,
242 +
206 243 // Booking Page Settings
207 244 'use_booking_page' => false,
208 245 'booking_page_id' => 0,
209 246
@@ -215,9 +252,10 @@
215 252 'seo_trip_meta_title' => '',
216 253 'seo_trip_meta_description' => '',
217 254 'seo_trip_meta_keywords' => '',
218 255 'seo_trip_meta_image' => 0,
219 -
256 + 'enable_sitemap' => true,
257 +
220 258 // Advanced Settings
221 259 'debug_mode' => false,
222 260 'enable_logging' => false,
223 261 'cache_enabled' => true,
@@ -357,15 +395,23 @@
357 395 {
358 396 try {
359 397 $settings = [];
360 398
361 - // Get all settings from WordPress options table with yatra_ prefix
399 + // Get all settings from WordPress options table with yatra_ prefix.
400 + // A sentinel default is essential here: get_option() returns boolean
401 + // false for a stored-false option just as it does for a missing one,
402 + // so checking `=== false` would reset every saved-off boolean back to
403 + // its default. That is exactly the "Show sold-out dates" bug — the
404 + // storefront honoured the saved value (isEnabled coerces '' -> false)
405 + // while the admin checkbox re-appeared enabled because this endpoint
406 + // handed React the default (true) instead of the saved false.
407 + $unset_sentinel = "\0__yatra_option_unset__\0";
362 408 foreach ($this->default_settings as $key => $default_value) {
363 409 $option_name = 'yatra_' . $key;
364 - $value = get_option($option_name, false);
365 -
366 - // Only use default if option doesn't exist (wasn't set by InstallerService)
367 - if ($value === false) {
410 + $value = get_option($option_name, $unset_sentinel);
411 +
412 + // Only use default when the option truly does not exist.
413 + if ($value === $unset_sentinel) {
368 414 $value = $default_value;
369 415 }
370 416
371 417 // Stored empty string should behave like "unset" for delivery identity (matches installer / backfill).
@@ -386,9 +432,18 @@
386 432 // Ensure arrays are returned as arrays (not objects)
387 433 if (is_array($default_value) && !is_array($value)) {
388 434 $value = [];
389 435 }
390 -
436 +
437 + // Boolean settings must round-trip to the admin as real booleans.
438 + // update_option() stores false as '' and the object cache can
439 + // return boolean false, so without this a disabled toggle would
440 + // reach React as '' / false and the checkbox (checked unless the
441 + // value is strictly !== false) would render enabled again.
442 + if (is_bool($default_value)) {
443 + $value = filter_var($value, FILTER_VALIDATE_BOOLEAN);
444 + }
445 +
391 446 $settings[$key] = $value;
392 447 }
393 448
394 449 // Special handling for booking_form_config - always use getBookingFormConfig which handles locked fields
@@ -413,8 +468,10 @@
413 468 'scheduled_payment_days',
414 469 'scheduled_payment_installments',
415 470 'scheduled_payment_interval',
416 471 'scheduled_payment_reminder_days',
472 + 'balance_anchor',
473 + 'balance_due_days',
417 474 ] as $sk
418 475 ) {
419 476 if (array_key_exists($sk, $this->default_settings)) {
420 477 $settings[$sk] = \Yatra\Services\SettingsService::get(
@@ -473,8 +530,10 @@
473 530 'scheduled_payment_days',
474 531 'scheduled_payment_installments',
475 532 'scheduled_payment_interval',
476 533 'scheduled_payment_reminder_days',
534 + 'balance_anchor',
535 + 'balance_due_days',
477 536 ];
478 537
479 538 // Collect flexible payment settings to delegate to Pro
480 539 $flexible_payment_settings = [];
@@ -1070,9 +1129,9 @@
1070 1129 private function sanitize_booking_form_config(array $config): array
1071 1130 {
1072 1131 $sanitized = [];
1073 1132 $allowed_form_types = ['contact_form', 'emergency_contact_form', 'traveler_form'];
1074 - $allowed_field_types = ['text', 'email', 'tel', 'date', 'select', 'country', 'textarea', 'checkbox', 'number'];
1133 + $allowed_field_types = ['text', 'email', 'tel', 'date', 'select', 'country', 'textarea', 'checkbox', 'number', 'text_block'];
1075 1134 $allowed_widths = ['full', 'half', 'third'];
1076 1135
1077 1136 foreach ($config as $form_type => $form_config) {
1078 1137 if (!in_array($form_type, $allowed_form_types, true)) {
@@ -1099,9 +1158,9 @@
1099 1158 'placeholder' => isset($field['placeholder']) ? sanitize_text_field($field['placeholder']) : '',
1100 1159 'required' => isset($field['required']) ? (bool) $field['required'] : false,
1101 1160 'enabled' => isset($field['enabled']) ? (bool) $field['enabled'] : true,
1102 1161 'order' => isset($field['order']) ? (int) $field['order'] : 0,
1103 - 'width' => in_array($field['width'] ?? 'full', $allowed_widths, true) ? $field['width'] : 'full',
1162 + 'width' => in_array($field['width'] ?? 'full', $allowed_widths, true) ? ($field['width'] ?? 'full') : 'full',
1104 1163 'locked' => isset($field['locked']) ? (bool) $field['locked'] : false,
1105 1164 ];
1106 1165
1107 1166 // Handle optional section
@@ -1107,8 +1166,18 @@
1107 1166 // Handle optional section
1108 1167 if (!empty($field['section'])) {
1109 1168 $sanitized_field['section'] = sanitize_key($field['section']);
1110 1169 }
1170 +
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 + }
1111 1180
1112 1181 // Handle options for select fields
1113 1182 if ($sanitized_field['type'] === 'select' && !empty($field['options']) && is_array($field['options'])) {
1114 1183 $sanitized_field['options'] = [];
@@ -1120,9 +1189,28 @@
1120 1189 ];
1121 1190 }
1122 1191 }
1123 1192 }
1124 -
1193 +
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 + }
1200 +
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;
1211 + }
1212 +
1125 1213 $sanitized[$form_type]['fields'][] = $sanitized_field;
1126 1214 }
1127 1215
1128 1216 // Sort fields by order