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 +79 -9 3.0.7trunk 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,8 +218,13 @@
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,
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,
205 227
206 228 // Search & Listing storefront UX. Defaults preserve current behaviour:
207 229 // every search field shown (true) and mobile filters expanded (false),
208 230 // so existing installs are unchanged until the owner opts in. Booleans
@@ -211,8 +233,12 @@
211 233 'search_show_destination' => true,
212 234 'search_show_activities' => true,
213 235 'search_show_duration' => true,
214 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,
215 241 'collapse_filters_on_mobile' => false,
216 242
217 243 // Booking Page Settings
218 244 'use_booking_page' => false,
@@ -226,9 +252,10 @@
226 252 'seo_trip_meta_title' => '',
227 253 'seo_trip_meta_description' => '',
228 254 'seo_trip_meta_keywords' => '',
229 255 'seo_trip_meta_image' => 0,
230 -
256 + 'enable_sitemap' => true,
257 +
231 258 // Advanced Settings
232 259 'debug_mode' => false,
233 260 'enable_logging' => false,
234 261 'cache_enabled' => true,
@@ -368,15 +395,23 @@
368 395 {
369 396 try {
370 397 $settings = [];
371 398
372 - // 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";
373 408 foreach ($this->default_settings as $key => $default_value) {
374 409 $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) {
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) {
379 414 $value = $default_value;
380 415 }
381 416
382 417 // Stored empty string should behave like "unset" for delivery identity (matches installer / backfill).
@@ -397,9 +432,18 @@
397 432 // Ensure arrays are returned as arrays (not objects)
398 433 if (is_array($default_value) && !is_array($value)) {
399 434 $value = [];
400 435 }
401 -
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 +
402 446 $settings[$key] = $value;
403 447 }
404 448
405 449 // Special handling for booking_form_config - always use getBookingFormConfig which handles locked fields
@@ -424,8 +468,10 @@
424 468 'scheduled_payment_days',
425 469 'scheduled_payment_installments',
426 470 'scheduled_payment_interval',
427 471 'scheduled_payment_reminder_days',
472 + 'balance_anchor',
473 + 'balance_due_days',
428 474 ] as $sk
429 475 ) {
430 476 if (array_key_exists($sk, $this->default_settings)) {
431 477 $settings[$sk] = \Yatra\Services\SettingsService::get(
@@ -484,8 +530,10 @@
484 530 'scheduled_payment_days',
485 531 'scheduled_payment_installments',
486 532 'scheduled_payment_interval',
487 533 'scheduled_payment_reminder_days',
534 + 'balance_anchor',
535 + 'balance_due_days',
488 536 ];
489 537
490 538 // Collect flexible payment settings to delegate to Pro
491 539 $flexible_payment_settings = [];
@@ -1110,9 +1158,9 @@
1110 1158 'placeholder' => isset($field['placeholder']) ? sanitize_text_field($field['placeholder']) : '',
1111 1159 'required' => isset($field['required']) ? (bool) $field['required'] : false,
1112 1160 'enabled' => isset($field['enabled']) ? (bool) $field['enabled'] : true,
1113 1161 'order' => isset($field['order']) ? (int) $field['order'] : 0,
1114 - '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',
1115 1163 'locked' => isset($field['locked']) ? (bool) $field['locked'] : false,
1116 1164 ];
1117 1165
1118 1166 // Handle optional section
@@ -1118,8 +1166,18 @@
1118 1166 // Handle optional section
1119 1167 if (!empty($field['section'])) {
1120 1168 $sanitized_field['section'] = sanitize_key($field['section']);
1121 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 + }
1122 1180
1123 1181 // Handle options for select fields
1124 1182 if ($sanitized_field['type'] === 'select' && !empty($field['options']) && is_array($field['options'])) {
1125 1183 $sanitized_field['options'] = [];
@@ -1137,8 +1195,20 @@
1137 1195 // keep its (safe-HTML) content, and it can never be required.
1138 1196 if ($sanitized_field['type'] === 'text_block') {
1139 1197 $sanitized_field['content'] = isset($field['content']) ? wp_kses_post($field['content']) : '';
1140 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;
1141 1211 }
1142 1212
1143 1213 $sanitized[$form_type]['fields'][] = $sanitized_field;
1144 1214 }