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
yatra / app / Controllers / SettingsController.php

SettingsController.php in Yatra – Travel Booking & Tour Operator Software trunk, at app/Controllers/SettingsController.php

1,405 lines 60.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Yatra\Controllers;
6
7 use WP_REST_Request;
8 use WP_REST_Response;
9 use WP_Error;
10 use Yatra\Services\EmailTemplatePreviewService;
11
12 /**
13 * Settings REST API Controller
14 * Handles getting and updating plugin settings stored in WordPress options table
15 */
16 class SettingsController extends BaseController
17 {
18 /**
19 * All settings fields with their default values
20 * Pro plugin can add additional settings via filter
21 */
22 private array $default_settings;
23
24 /**
25 * Constructor - initialize default settings with filter
26 */
27 public function __construct()
28 {
29 $wpAdminEmail = (string) get_option('admin_email', '');
30 $wpSiteName = (string) get_bloginfo('name');
31
32 // Define base settings
33 $base_settings = [
34 // General Settings
35 'company_name' => '',
36 'company_email' => '',
37 'company_phone' => '',
38 'company_address' => '',
39 'company_city' => '',
40 'company_state' => '',
41 'company_country' => '',
42 'company_zip' => '',
43 'company_website' => '',
44 'company_logo' => '',
45 'timezone' => 'UTC',
46 'date_format' => 'Y-m-d',
47 'time_format' => 'H:i',
48 'frontend_primary_color' => '#3b82f6',
49 'frontend_container_max_width' => '',
50
51 // Booking Settings
52 'booking_confirmation' => true,
53 'auto_confirm_bookings' => false,
54 'auto_confirm_pay_later' => true,
55 'require_login' => false,
56 'allow_guest_checkout' => true,
57 // cancellation_policy / cancellation_days / refund_policy were
58 // removed in 3.0.5 — they only inserted text into the booking
59 // confirmation email but did NOT enforce a cancellation cutoff
60 // because Yatra has no customer-facing self-service
61 // cancellation flow. Per-trip cancellation copy on the Trip
62 // editor is the supported way to communicate policy. If those
63 // legacy options still exist in wp_options on upgraded sites
64 // they're harmless orphans — the save endpoint no longer
65 // accepts them, and the email template skips the cancellation
66 // paragraph when the global setting is absent.
67 'booking_expiry_hours' => 24,
68 'booking_reminder_days' => 3,
69 'allow_waitlist' => true,
70 'waitlist_auto_confirm' => false,
71 // Pro: render available departure dates as a <select> instead of a
72 // flatpickr calendar on the single-trip sidebar (desktop + mobile).
73 'date_picker_as_dropdown' => false,
74
75 // Payment Settings
76 'currency' => 'USD',
77 'payment_test_mode' => true,
78 'payment_gateways' => [],
79 'payment_methods' => [],
80 'partial_payment' => false,
81 'partial_payment_percentage' => 30,
82 'deposit_required' => false,
83 'deposit_percentage' => 20,
84 'gateway_configs' => [],
85 'gateway_order' => [],
86
87 // Discount Stacking Mode — controls how the Advanced Discount and
88 // Dynamic Pricing modules combine when both can fire on the same
89 // booking. Default 'both' preserves the legacy stacked behavior
90 // (discount on top of DP-adjusted price). The Settings → Pricing
91 // tab only surfaces this setting when BOTH modules are enabled,
92 // and CalculationService only enforces a non-default mode when
93 // BOTH modules are loaded — so sites with only one (or neither)
94 // module see zero behavior change.
95 //
96 // Allowed: 'both' | 'discount_only' | 'dynamic_pricing_only' | 'best_for_customer'
97 'discount_stacking_mode' => 'both',
98
99 // Scheduled/Recurring Payment Settings (Pro feature - defaults disabled)
100 'enable_scheduled_payments' => false,
101 'scheduled_payment_type' => 'single', // single, installments
102 'scheduled_payment_days' => 15, // Days until first scheduled payment
103 'scheduled_payment_installments' => 1, // Number of installments (if type is installments)
104 'scheduled_payment_interval' => 30, // Days between installments
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
108 'allow_save_payment_methods' => false,
109
110 // Email Settings (WordPress site defaults when Yatra options are missing)
111 'admin_email' => $wpAdminEmail,
112 'from_email' => $wpAdminEmail,
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' => '',
117 'email_template_booking' => true,
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,
122 'email_template_cancellation' => true,
123 'email_template_reminder' => true,
124 'email_template_admin_new_booking' => true,
125 'email_template_admin_payment' => true,
126 'email_template_admin_cancellation' => true,
127 'email_template_trip_consent' => true,
128 'email_template_customer_verification' => true,
129 'email_template_guest_verification' => true,
130 'email_template_account_email_change' => true,
131 'email_template_account_email_changed' => true,
132 'email_template_booking_completed' => true,
133 'email_template_booking_expired_customer' => true,
134 'email_template_admin_booking_expired' => true,
135 'email_template_scheduled_payment_reminder' => true,
136 'email_template_scheduled_payment_succeeded' => true,
137 'email_template_scheduled_payment_failed' => true,
138 'email_template_admin_scheduled_payment_failed' => true,
139 'email_template_enquiry_received' => true,
140 'email_template_enquiry_admin' => true,
141 'email_template_enquiry_response' => true,
142 'email_template_review_request' => true,
143 'email_template_abandoned_booking_recovery_first' => true,
144 'email_template_abandoned_booking_recovery_second' => true,
145 'email_template_abandoned_booking_recovery_final' => true,
146 'smtp_enabled' => false,
147 'smtp_host' => 'smtp.gmail.com',
148 'smtp_port' => 587,
149 'smtp_username' => '',
150 'smtp_password' => '',
151 'smtp_encryption' => 'tls',
152
153 // Customer Settings
154 'customer_registration' => true,
155 'customer_fields' => [],
156 'require_email_verification' => false,
157 // Per-booking verification for guest checkouts. Distinct from the
158 // account-creation `require_email_verification` flag because a guest
159 // never registers — the verification is gated on the booking itself
160 // (BookingSessionController checks this when admitting a guest).
161 'require_guest_email_verification' => false,
162 'customer_account_page' => '',
163 'allow_customer_reviews' => true,
164 'customer_dashboard_enabled' => true,
165
166 // Review Settings
167 'enable_reviews' => true,
168 'require_booking' => true,
169 'auto_approve_reviews' => false,
170 'review_moderation' => true,
171 'min_rating' => 1,
172 'allow_anonymous_reviews' => false,
173 'review_reminder_days' => 7,
174
175 // Tax Settings
176 'enable_tax' => false,
177 'tax_name' => __('Tax', 'yatra'),
178 'tax_rate' => 0,
179 'tax_inclusive' => false,
180 'vat_number' => '',
181 'tax_by_country' => false,
182 'tax_rates' => [],
183 'multiple_taxes_enabled' => false,
184 'multiple_taxes' => [],
185 'multiple_taxes_by_country' => [],
186
187 // Currency Settings
188 'default_currency' => 'USD',
189 'multi_currency' => false,
190 'currency_position' => 'left',
191 'currency_decimals' => 2,
192 'decimal_separator'=>'.',
193 'thousand_separator'=>',',
194
195 // Notification Settings (SMS / future channels — booking email toggles live under Email → Templates)
196 'sms_notifications' => false,
197 'sms_provider' => '',
198 'sms_api_key' => '',
199
200 // Integration Settings
201 'google_analytics' => '',
202 'facebook_pixel' => '',
203 'recaptcha_enabled' => false,
204 'recaptcha_site_key' => '',
205 'recaptcha_secret_key' => '',
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
214 // Permalink Settings
215 'trip_base' => 'trip',
216 'destination_base' => 'destination',
217 'activity_base' => 'activity',
218 'trip_category_base' => 'trip-category',
219 'booking_base' => 'book',
220 // Wishlist (Pro) — stored in free options; active only when Pro + setting on
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,
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
243 // Booking Page Settings
244 'use_booking_page' => false,
245 'booking_page_id' => 0,
246
247 // Legal Pages (Booking UI)
248 'terms_page_id' => 0,
249 'privacy_policy_page_id' => 0,
250
251 // SEO Settings
252 'seo_trip_meta_title' => '',
253 'seo_trip_meta_description' => '',
254 'seo_trip_meta_keywords' => '',
255 'seo_trip_meta_image' => 0,
256 'enable_sitemap' => true,
257
258 // Advanced Settings
259 'debug_mode' => false,
260 'enable_logging' => false,
261 'cache_enabled' => true,
262 'api_key' => '',
263 'api_rate_limit' => 100,
264 'session_timeout' => 3600,
265
266 // Booking Form Builder
267 'booking_form_config' => [],
268 ];
269
270 $base_settings = array_merge(
271 $base_settings,
272 \Yatra\Services\EmailTemplateDefaults::settingsOptionDefaults()
273 );
274
275 // Allow Pro plugins to add their settings via filter
276 $this->default_settings = apply_filters('yatra_settings_default_fields', $base_settings);
277 }
278
279 public function register_routes(): void
280 {
281 $namespace = 'yatra/v1';
282 $base = 'settings';
283
284 register_rest_route($namespace, '/' . $base, [
285 [
286 'methods' => \WP_REST_Server::READABLE,
287 'callback' => [$this, 'get_settings'],
288 'permission_callback' => [$this, 'check_permission'],
289 ],
290 [
291 'methods' => \WP_REST_Server::EDITABLE,
292 'callback' => [$this, 'update_settings'],
293 'permission_callback' => [$this, 'check_permission'],
294 ],
295 ]);
296
297 // Flush rewrite rules endpoint
298 register_rest_route($namespace, '/' . $base . '/flush-rewrite-rules', [
299 [
300 'methods' => \WP_REST_Server::CREATABLE,
301 'callback' => [$this, 'flush_rewrite_rules'],
302 'permission_callback' => [$this, 'check_permission'],
303 ],
304 ]);
305
306 // Get WordPress pages for booking page selection
307 register_rest_route($namespace, '/' . $base . '/pages', [
308 [
309 'methods' => \WP_REST_Server::READABLE,
310 'callback' => [$this, 'get_pages'],
311 'permission_callback' => [$this, 'check_permission'],
312 ],
313 ]);
314
315 // Check if page has booking shortcode
316 register_rest_route($namespace, '/' . $base . '/check-shortcode/(?P<page_id>\d+)', [
317 [
318 'methods' => \WP_REST_Server::READABLE,
319 'callback' => [$this, 'check_booking_shortcode'],
320 'permission_callback' => [$this, 'check_permission'],
321 ],
322 ]);
323
324 // Insert booking shortcode into page
325 register_rest_route($namespace, '/' . $base . '/insert-shortcode/(?P<page_id>\d+)', [
326 [
327 'methods' => \WP_REST_Server::CREATABLE,
328 'callback' => [$this, 'insert_booking_shortcode'],
329 'permission_callback' => [$this, 'check_permission'],
330 ],
331 ]);
332
333 register_rest_route($namespace, '/' . $base . '/email-template-preview', [
334 [
335 'methods' => \WP_REST_Server::CREATABLE,
336 'callback' => [$this, 'preview_core_email_template'],
337 'permission_callback' => [$this, 'check_permission'],
338 ],
339 ]);
340 }
341
342 /**
343 * Preview a core (settings-backed) transactional template with sample merge data.
344 */
345 public function preview_core_email_template(WP_REST_Request $request)
346 {
347 try {
348 $params = $request->get_json_params();
349 if (!is_array($params)) {
350 return $this->error_response(__('Invalid request body.', 'yatra'), 400);
351 }
352
353 $templateKey = sanitize_key($params['template_key'] ?? '');
354 $subjectTpl = sanitize_text_field($params['subject'] ?? '');
355 $bodyTpl = wp_kses_post($params['body'] ?? '');
356 $tripId = isset($params['trip_id']) ? (int) $params['trip_id'] : 0;
357 $tripId = $tripId > 0 ? $tripId : null;
358
359 $rendered = EmailTemplatePreviewService::render($templateKey, $subjectTpl, $bodyTpl, $tripId);
360
361 return $this->success_response([
362 'success' => true,
363 'data' => [
364 'subject' => $rendered['subject'],
365 'body' => $rendered['body'],
366 ],
367 ]);
368 } catch (\InvalidArgumentException $e) {
369 return $this->error_response($e->getMessage(), 400);
370 } catch (\Exception $e) {
371 return $this->error_response($e->getMessage(), 500);
372 }
373 }
374
375 /**
376 * Plugin settings — high-sensitivity cap. By default only the
377 * Owner role holds `yatra_manage_settings` (Manager doesn't, by
378 * design — settings include payment gateway routing, email
379 * delivery configuration, currency formatting and similar
380 * global behaviour). WP admins pass via the Team module's
381 * admin-fallback filter.
382 */
383 public function check_permission(?WP_REST_Request $request = null): bool
384 {
385 if (!is_user_logged_in()) {
386 return false;
387 }
388 return current_user_can('yatra_manage_settings');
389 }
390
391 /**
392 * Get all settings
393 */
394 public function get_settings(WP_REST_Request $request)
395 {
396 try {
397 $settings = [];
398
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";
408 foreach ($this->default_settings as $key => $default_value) {
409 $option_name = 'yatra_' . $key;
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) {
414 $value = $default_value;
415 }
416
417 // Stored empty string should behave like "unset" for delivery identity (matches installer / backfill).
418 if (($key === 'admin_email' || $key === 'from_email') && is_string($value) && trim($value) === '') {
419 $wp = (string) get_option('admin_email', '');
420 $value = $wp !== '' ? $wp : $value;
421 }
422 if ($key === 'from_name' && is_string($value) && trim($value) === '') {
423 $wp = (string) get_bloginfo('name');
424 $value = $wp !== '' ? $wp : $value;
425 }
426
427 // Handle serialized arrays (for fields like payment_gateways, customer_fields, etc.)
428 if (is_string($value) && is_serialized($value)) {
429 $value = maybe_unserialize($value);
430 }
431
432 // Ensure arrays are returned as arrays (not objects)
433 if (is_array($default_value) && !is_array($value)) {
434 $value = [];
435 }
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
446 $settings[$key] = $value;
447 }
448
449 // Special handling for booking_form_config - always use getBookingFormConfig which handles locked fields
450 $settings['booking_form_config'] = \Yatra\Services\SettingsService::getBookingFormConfig();
451
452 // Merge in flexible payment settings from Pro module if enabled
453 $flexible_payment_settings = apply_filters('yatra_get_flexible_payment_settings', []);
454 if (!empty($flexible_payment_settings)) {
455 $settings = array_merge($settings, $flexible_payment_settings);
456 }
457
458 $scheduled_payment_settings = apply_filters('yatra_get_scheduled_payment_settings', []);
459 if (!empty($scheduled_payment_settings)) {
460 $settings = array_merge($settings, $scheduled_payment_settings);
461 }
462
463 // Scheduled payment keys are owned by Pro (yatra_pro_scheduled_payments), not yatra_* options.
464 foreach (
465 [
466 'enable_scheduled_payments',
467 'scheduled_payment_type',
468 'scheduled_payment_days',
469 'scheduled_payment_installments',
470 'scheduled_payment_interval',
471 'scheduled_payment_reminder_days',
472 'balance_anchor',
473 'balance_due_days',
474 ] as $sk
475 ) {
476 if (array_key_exists($sk, $this->default_settings)) {
477 $settings[$sk] = \Yatra\Services\SettingsService::get(
478 $sk,
479 $this->default_settings[$sk]
480 );
481 }
482 }
483
484 $settings = $this->syncAccountRouteSettingsForResponse($settings);
485
486 /**
487 * Allow Pro modules to align REST payloads with canonical option stores
488 * (e.g. GA4 settings that also live in yatra_google_analytics_settings).
489 */
490 $settings = apply_filters('yatra_rest_settings', $settings);
491
492 return $this->success_response($settings);
493 } catch (\Exception $e) {
494 return $this->error_response($e->getMessage(), 500);
495 }
496 }
497
498 /**
499 * Update settings
500 */
501 public function update_settings(WP_REST_Request $request)
502 {
503 try {
504 $data = $request->get_json_params();
505
506 if (!is_array($data)) {
507 return $this->error_response('Invalid settings data', 400);
508 }
509
510 $updated = [];
511 $errors = [];
512
513 // Check if Dynamic Form Field module is enabled
514 $is_dynamic_form_enabled = apply_filters('yatra_dynamic_form_field_enabled', false);
515
516 // Check if Flexible Payments module is enabled (Pro feature)
517 $is_flexible_payments_enabled = apply_filters('yatra_flexible_payments_enabled', false);
518
519 $is_scheduled_payments_module = apply_filters('yatra_scheduled_payments_module_active', false);
520
521 // Flexible payment settings keys (Pro only)
522 $flexible_payment_keys = [
523 'deposit_required', 'deposit_percentage', 'partial_payment',
524 'partial_payment_percentage', 'enable_deposit', 'allow_save_payment_methods',
525 ];
526
527 $scheduled_payment_keys = [
528 'enable_scheduled_payments',
529 'scheduled_payment_type',
530 'scheduled_payment_days',
531 'scheduled_payment_installments',
532 'scheduled_payment_interval',
533 'scheduled_payment_reminder_days',
534 'balance_anchor',
535 'balance_due_days',
536 ];
537
538 // Collect flexible payment settings to delegate to Pro
539 $flexible_payment_settings = [];
540
541 $scheduled_payment_settings_batch = [];
542
543 // Process each setting
544 foreach ($data as $key => $value) {
545 // Skip booking_form_config if Dynamic Form Field module is not enabled
546 // This allows the settings to save without error when the module is disabled
547 if ($key === 'booking_form_config' && !$is_dynamic_form_enabled) {
548 continue;
549 }
550
551 // Delegate flexible payment settings to Pro module
552 if (in_array($key, $flexible_payment_keys, true)) {
553 if ($is_flexible_payments_enabled) {
554 $flexible_payment_settings[$key] = $value;
555 }
556 // Skip saving in Free plugin - Pro handles these
557 continue;
558 }
559
560 if (in_array($key, $scheduled_payment_keys, true)) {
561 if ($is_scheduled_payments_module) {
562 $scheduled_payment_settings_batch[$key] = $value;
563 }
564 continue;
565 }
566
567 // Wishlist toggle: only meaningful with Yatra Pro active
568 if ($key === 'enable_wishlist' && !apply_filters('yatra_is_pro_active', false)) {
569 continue;
570 }
571
572 // Validate that the key exists in default settings
573 if (!array_key_exists($key, $this->default_settings)) {
574 $errors[] = sprintf('Unknown setting: %s', $key);
575 continue;
576 }
577
578 // Sanitize and validate the value based on its type
579 $sanitized_value = $this->sanitize_setting($key, $value);
580
581 if ($sanitized_value === null) {
582 $errors[] = sprintf('Invalid value for setting: %s', $key);
583 continue;
584 }
585
586 // Save to WordPress options table with yatra_ prefix
587 $option_name = 'yatra_' . $key;
588
589 // Serialize arrays for storage
590 if (is_array($sanitized_value)) {
591 $sanitized_value = maybe_serialize($sanitized_value);
592 }
593
594 $result = update_option($option_name, $sanitized_value);
595
596 if ($result !== false) {
597 $updated[] = $key;
598 }
599 }
600
601 // Delegate flexible payment settings to Pro module for saving
602 if (!empty($flexible_payment_settings) && $is_flexible_payments_enabled) {
603 do_action('yatra_save_flexible_payment_settings', $flexible_payment_settings);
604 $updated = array_merge($updated, array_keys($flexible_payment_settings));
605 }
606
607 if (!empty($scheduled_payment_settings_batch) && $is_scheduled_payments_module) {
608 do_action('yatra_save_scheduled_payment_settings', $scheduled_payment_settings_batch);
609 $updated = array_merge($updated, array_keys($scheduled_payment_settings_batch));
610 }
611
612 // Sync currency keys: keep 'currency' and 'default_currency' in sync
613 // Admin UI has both Payment Settings (currency) and Currency Settings (default_currency)
614 if (in_array('default_currency', $updated, true) && !in_array('currency', $updated, true)) {
615 $sync_currency = get_option('yatra_default_currency', 'USD');
616 update_option('yatra_currency', $sync_currency);
617 } elseif (in_array('currency', $updated, true) && !in_array('default_currency', $updated, true)) {
618 $sync_currency = get_option('yatra_currency', 'USD');
619 update_option('yatra_default_currency', $sync_currency);
620 }
621
622 if (in_array('customer_account_page', $updated, true)) {
623 $this->persistAccountBaseFromCustomerAccountPage();
624 }
625
626 if (!empty($errors)) {
627 $errorSummary = implode('; ', $errors);
628 return $this->error_response(
629 sprintf('Some settings could not be updated: %s', $errorSummary),
630 400,
631 [
632 'errors' => $errors,
633 'updated' => $updated,
634 ]
635 );
636 }
637
638 // Flush rewrite rules if permalink settings were updated
639 if (in_array('trip_base', $updated, true) ||
640 in_array('destination_base', $updated, true) ||
641 in_array('activity_base', $updated, true) ||
642 in_array('trip_category_base', $updated, true) ||
643 in_array('booking_base', $updated, true) ||
644 in_array('use_booking_page', $updated, true) ||
645 in_array('booking_page_id', $updated, true) ||
646 in_array('customer_account_page', $updated, true)) {
647 // Use hard flush to ensure rules are saved to database
648 flush_rewrite_rules(true);
649 }
650
651 if (!empty($updated)) {
652 \Yatra\Services\SettingsService::reload();
653 }
654
655 // Cross-validation: booking-auth settings interact via OR
656 // logic in booking-content.php, so some combinations are
657 // semantically inconsistent or redundant. We don't block
658 // the save (the resulting state still has well-defined
659 // behavior), but we surface a clear notice so the operator
660 // understands what they just configured.
661 //
662 // require_login=true + allow_guest_checkout=true →
663 // require_login wins; allow_guest_checkout is a no-op.
664 // require_login=true + allow_guest_checkout=false →
665 // Strictest setting (login required, no guest path).
666 // Internally consistent.
667 // require_login=false + allow_guest_checkout=false →
668 // Guests blocked, logged-in users can book. Consistent.
669 // require_login=false + allow_guest_checkout=true →
670 // Default. Permissive.
671 $notices = [];
672 $effective_require_login = \array_key_exists('require_login', $data)
673 ? (bool) $data['require_login']
674 : (bool) \Yatra\Services\SettingsService::get('require_login', false);
675 $effective_allow_guest = \array_key_exists('allow_guest_checkout', $data)
676 ? (bool) $data['allow_guest_checkout']
677 : (bool) \Yatra\Services\SettingsService::get('allow_guest_checkout', true);
678
679 if ($effective_require_login && $effective_allow_guest) {
680 $notices[] = [
681 'level' => 'warning',
682 'code' => 'booking_auth_redundant',
683 'message' => __(
684 'Heads up: "Require login" is on, so "Allow guest checkout" has no effect — every customer will need to log in to book. To accept guests, turn "Require login" off.',
685 'yatra'
686 ),
687 ];
688 }
689
690 // Scheduled Payments + guest checkout — incompatible at
691 // the gateway level. Scheduled charges require a saved
692 // payment-method tied to a customer record on the
693 // gateway side (Stripe Customer, etc.), which in turn
694 // requires a logged-in WP user. When both settings are
695 // on, the system gracefully skips installment creation
696 // for guest bookings — but operators expect them to
697 // work and only discover the gap when reconciling
698 // unpaid bookings weeks later. Surface this proactively.
699 $effective_scheduled_payments = \array_key_exists('enable_scheduled_payments', $data)
700 ? (bool) $data['enable_scheduled_payments']
701 : (bool) \Yatra\Services\SettingsService::get('enable_scheduled_payments', false);
702 if (
703 $effective_scheduled_payments
704 && $effective_allow_guest
705 && !$effective_require_login
706 ) {
707 $notices[] = [
708 'level' => 'info',
709 'code' => 'scheduled_payments_guest_caveat',
710 'message' => __(
711 'Scheduled Payments is on with guest checkout allowed. Scheduled installments only run for bookings made by logged-in customers (they need a saved payment method tied to their account). Guest bookings will be charged in full at checkout instead. Turn on "Require login" if every booking must support installments.',
712 'yatra'
713 ),
714 ];
715 }
716
717 $response = [
718 'message' => 'Settings updated successfully',
719 'updated' => $updated,
720 ];
721 if ($notices !== []) {
722 $response['notices'] = $notices;
723 }
724 return $this->success_response($response);
725 } catch (\Exception $e) {
726 return $this->error_response($e->getMessage(), 500);
727 }
728 }
729
730 /**
731 * Sanitize and validate setting value
732 * Pro plugins can handle sanitization of their own settings via filter
733 *
734 * @param mixed $value
735 * @return mixed
736 */
737 private function sanitize_setting(string $key, $value)
738 {
739 $default = $this->default_settings[$key] ?? null;
740 $default_type = gettype($default);
741
742 // Allow Pro plugins to handle sanitization of their own settings
743 $filtered_value = apply_filters('yatra_sanitize_setting', null, $key, $value, $default);
744 if ($filtered_value !== null) {
745 return $filtered_value;
746 }
747
748 // Handle null values - use default
749 if ($value === null) {
750 return $default;
751 }
752
753 // Handle arrays
754 if (is_array($default)) {
755 if (!is_array($value)) {
756 return null;
757 }
758 // Sanitize array values
759 return array_map(function($item) {
760 if (is_string($item)) {
761 return sanitize_text_field($item);
762 }
763 if (is_numeric($item)) {
764 return is_float($item) ? (float) $item : (int) $item;
765 }
766 if (is_bool($item)) {
767 return (bool) $item;
768 }
769 if (is_array($item)) {
770 return $this->sanitize_array($item);
771 }
772 return $item;
773 }, $value);
774 }
775
776 // Handle booleans (REST may send true/false strings)
777 if (is_bool($default)) {
778 if (is_bool($value)) {
779 return $value;
780 }
781 if (is_string($value)) {
782 $parsed = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
783 return $parsed !== null ? $parsed : (bool) $value;
784 }
785 return (bool) $value;
786 }
787
788 // Handle integers
789 if (is_int($default)) {
790 if (!is_numeric($value)) {
791 return null;
792 }
793 $int_value = (int) $value;
794 // Validate ranges for specific fields
795 if ($key === 'booking_expiry_hours' && $int_value < 0) {
796 return null;
797 }
798 if ($key === 'partial_payment_percentage' && ($int_value < 0 || $int_value > 100)) {
799 return null;
800 }
801 if ($key === 'deposit_percentage' && ($int_value < 0 || $int_value > 100)) {
802 return null;
803 }
804 if ($key === 'tax_rate' && ($int_value < 0 || $int_value > 100)) {
805 return null;
806 }
807 if ($key === 'smtp_port' && ($int_value < 1 || $int_value > 65535)) {
808 return null;
809 }
810 return $int_value;
811 }
812
813 // Handle floats
814 if (is_float($default)) {
815 if (!is_numeric($value)) {
816 return null;
817 }
818 $float_value = (float) $value;
819 if ($float_value < 0) {
820 return null;
821 }
822 return $float_value;
823 }
824
825 // Handle strings
826 if (is_string($default)) {
827 if ($key === 'timezone') {
828 $tz = is_string($value) ? trim($value) : '';
829 if ($tz === '') {
830 return is_string($default) ? $default : 'UTC';
831 }
832 try {
833 new \DateTimeZone($tz);
834
835 return $tz;
836 } catch (\Exception $e) {
837 return is_string($default) ? $default : 'UTC';
838 }
839 }
840 if ($key === 'currency_position') {
841 $allowed = ['left', 'right', 'left_space', 'right_space', 'before', 'after'];
842 $v = is_string($value) ? strtolower(trim($value)) : '';
843
844 return in_array($v, $allowed, true) ? $v : (is_string($default) ? $default : 'left');
845 }
846 if ($key === 'discount_stacking_mode') {
847 // Strict enum — any other value silently falls back to the
848 // backward-compatible default so a malformed POST cannot
849 // change pricing behavior unexpectedly.
850 $allowed = ['both', 'discount_only', 'dynamic_pricing_only', 'best_for_customer'];
851 $v = is_string($value) ? strtolower(trim($value)) : '';
852
853 return in_array($v, $allowed, true) ? $v : 'both';
854 }
855 // Special handling for specific fields
856 if ($key === 'company_email' || $key === 'admin_email' || $key === 'from_email' || $key === 'smtp_username') {
857 return sanitize_email($value);
858 }
859 if ($key === 'company_website' || $key === 'company_logo' || $key === 'google_analytics' || $key === 'facebook_pixel') {
860 return esc_url_raw($value);
861 }
862 if ($key === 'seo_trip_meta_title') {
863 // Allow more characters for meta title, but strip HTML
864 return wp_strip_all_tags($value);
865 }
866 if ($key === 'seo_trip_meta_description') {
867 // Allow more characters for meta description, but strip HTML
868 return wp_strip_all_tags($value);
869 }
870 if ($key === 'seo_trip_meta_keywords') {
871 // Allow keywords, strip HTML and sanitize
872 return sanitize_text_field($value);
873 }
874 if ($key === 'frontend_primary_color') {
875 return \Yatra\Utils\FrontendThemeCss::sanitizePrimaryColor(is_string($value) ? $value : '');
876 }
877 if ($key === 'frontend_container_max_width') {
878 return \Yatra\Utils\FrontendThemeCss::sanitizeContainerMaxWidthSetting(
879 is_string($value) ? $value : ''
880 );
881 }
882 if (is_string($key) && strpos($key, 'email_tpl_') === 0 && substr($key, -5) === '_body') {
883 return wp_kses_post((string) $value);
884 }
885 if (is_string($key) && strpos($key, 'email_tpl_') === 0 && substr($key, -8) === '_subject') {
886 return sanitize_text_field((string) $value);
887 }
888 if ($key === 'smtp_password' || $key === 'api_key' || $key === 'sms_api_key' || $key === 'recaptcha_secret_key') {
889 // Don't sanitize passwords/keys too aggressively
890 return sanitize_text_field($value);
891 }
892 if ($key === 'gateway_configs') {
893 // Handle nested array structure for gateway configs
894 if (is_array($value)) {
895 return $this->sanitize_gateway_configs($value);
896 }
897 return [];
898 }
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 if ($key === 'tax_rates') {
907 // Handle nested array structure for tax rates
908 if (is_array($value)) {
909 return $this->sanitize_tax_rates($value);
910 }
911 return [];
912 }
913 return sanitize_text_field($value);
914 }
915
916 return $value;
917 }
918
919 /**
920 * Sanitize nested array
921 */
922 private function sanitize_array(array $array): array
923 {
924 $sanitized = [];
925 foreach ($array as $k => $v) {
926 $sanitized_key = is_string($k) ? sanitize_key($k) : $k;
927 if (is_array($v)) {
928 $sanitized[$sanitized_key] = $this->sanitize_array($v);
929 } elseif (is_string($v)) {
930 $sanitized[$sanitized_key] = sanitize_text_field($v);
931 } elseif (is_numeric($v)) {
932 $sanitized[$sanitized_key] = is_float($v) ? (float) $v : (int) $v;
933 } elseif (is_bool($v)) {
934 $sanitized[$sanitized_key] = (bool) $v;
935 } else {
936 $sanitized[$sanitized_key] = $v;
937 }
938 }
939 return $sanitized;
940 }
941
942 /**
943 * Sanitize gateway configs
944 */
945 private function sanitize_gateway_configs(array $configs): array
946 {
947 $sanitized = [];
948 foreach ($configs as $gateway => $config) {
949 if (!is_array($config)) {
950 continue;
951 }
952 $sanitized_gateway = sanitize_key($gateway);
953 $row = [
954 'enabled' => isset($config['enabled']) ? (bool) $config['enabled'] : false,
955 'icon' => isset($config['icon']) ? esc_url_raw($config['icon']) : '',
956 'title' => isset($config['title']) ? sanitize_text_field($config['title']) : '',
957 'description' => isset($config['description']) ? sanitize_textarea_field($config['description']) : '',
958 'api_key' => isset($config['api_key']) ? sanitize_text_field($config['api_key']) : '',
959 'api_secret' => isset($config['api_secret']) ? sanitize_text_field($config['api_secret']) : '',
960 'client_id' => isset($config['client_id']) ? sanitize_text_field($config['client_id']) : '',
961 'client_secret' => isset($config['client_secret']) ? sanitize_text_field($config['client_secret']) : '',
962 'merchant_id' => isset($config['merchant_id']) ? sanitize_text_field($config['merchant_id']) : '',
963 'public_key' => isset($config['public_key']) ? sanitize_text_field($config['public_key']) : '',
964 'private_key' => isset($config['private_key']) ? sanitize_text_field($config['private_key']) : '',
965 'webhook_secret' => isset($config['webhook_secret']) ? sanitize_text_field($config['webhook_secret']) : '',
966 'test_mode' => isset($config['test_mode']) ? (bool) $config['test_mode'] : false,
967 'sandbox' => isset($config['sandbox']) ? (bool) $config['sandbox'] : false,
968 ];
969
970 if ($sanitized_gateway === 'paypal') {
971 $mode = isset($config['mode']) && in_array((string) $config['mode'], ['simple', 'advanced'], true)
972 ? (string) $config['mode']
973 : 'simple';
974 $row['email'] = isset($config['email']) ? sanitize_email((string) $config['email']) : '';
975 $row['mode'] = $mode;
976 }
977
978 if ($sanitized_gateway === 'pay_later') {
979 $row['payment_deadline_days'] = isset($config['payment_deadline_days'])
980 ? max(1, min(60, (int) $config['payment_deadline_days']))
981 : 7;
982 $row['auto_cancel_days'] = isset($config['auto_cancel_days'])
983 ? max(0, min(30, (int) $config['auto_cancel_days']))
984 : 3;
985 $row['require_deposit'] = isset($config['require_deposit']) ? (bool) $config['require_deposit'] : false;
986 $row['deposit_amount'] = isset($config['deposit_amount'])
987 ? max(1, min(50, (int) $config['deposit_amount']))
988 : 10;
989 $row['reminder_days'] = isset($config['reminder_days'])
990 ? sanitize_text_field((string) $config['reminder_days'])
991 : '7,3,1';
992 }
993
994 if ($sanitized_gateway === 'stripe') {
995 $allowedStripeMethods = ['card', 'google_pay', 'apple_pay'];
996 $methodsRaw = isset($config['enabled_methods']) ? (string) $config['enabled_methods'] : '';
997 if ($methodsRaw !== '') {
998 $parts = array_filter(array_map('trim', explode(',', $methodsRaw)));
999 $normalized = [];
1000 foreach ($parts as $part) {
1001 $slug = strtolower($part);
1002 if (in_array($slug, $allowedStripeMethods, true)) {
1003 $normalized[] = $slug;
1004 }
1005 }
1006 $row['enabled_methods'] = $normalized !== [] ? implode(',', $normalized) : 'card,google_pay,apple_pay';
1007 } else {
1008 $row['enabled_methods'] = 'card,google_pay,apple_pay';
1009 }
1010 foreach (['live_publishable_key', 'live_secret_key', 'test_publishable_key', 'test_secret_key'] as $stripeKey) {
1011 if (array_key_exists($stripeKey, $config)) {
1012 $row[$stripeKey] = sanitize_text_field((string) $config[$stripeKey]);
1013 }
1014 }
1015 }
1016
1017 if ($sanitized_gateway === 'razorpay') {
1018 $row['key_id'] = isset($config['key_id']) ? sanitize_text_field((string) $config['key_id']) : '';
1019 $row['key_secret'] = isset($config['key_secret']) ? sanitize_text_field((string) $config['key_secret']) : '';
1020 }
1021
1022 if ($sanitized_gateway === 'mollie') {
1023 $row['api_key'] = isset($config['api_key']) ? sanitize_text_field((string) $config['api_key']) : '';
1024 $row['webhook_url'] = isset($config['webhook_url']) ? esc_url_raw((string) $config['webhook_url']) : '';
1025 $allowedMollie = ['creditcard', 'ideal', 'bancontact', 'sofort', 'eps', 'giropay', 'paypal', 'sepadirectdebit'];
1026 $row['payment_methods'] = $this->sanitizeGatewayStringList(
1027 $config['payment_methods'] ?? [],
1028 $allowedMollie,
1029 ['creditcard', 'ideal', 'paypal']
1030 );
1031 }
1032
1033 if ($sanitized_gateway === 'paystack') {
1034 $row['public_key'] = isset($config['public_key']) ? sanitize_text_field((string) $config['public_key']) : '';
1035 $row['secret_key'] = isset($config['secret_key']) ? sanitize_text_field((string) $config['secret_key']) : '';
1036 $row['webhook_url'] = isset($config['webhook_url']) ? esc_url_raw((string) $config['webhook_url']) : '';
1037 $allowedPaystack = ['card', 'bank', 'ussd', 'qr', 'mobile_money', 'bank_transfer'];
1038 $row['payment_channels'] = $this->sanitizeGatewayStringList(
1039 $config['payment_channels'] ?? [],
1040 $allowedPaystack,
1041 ['card', 'bank', 'ussd']
1042 );
1043 unset($row['private_key']);
1044 }
1045
1046 if ($sanitized_gateway === 'square') {
1047 $row['application_id'] = isset($config['application_id']) ? sanitize_text_field((string) $config['application_id']) : '';
1048 $row['access_token'] = isset($config['access_token']) ? sanitize_text_field((string) $config['access_token']) : '';
1049 $row['location_id'] = isset($config['location_id']) ? sanitize_text_field((string) $config['location_id']) : '';
1050 }
1051
1052 if ($sanitized_gateway === 'authorize_net') {
1053 $row['api_login_id'] = isset($config['api_login_id']) ? sanitize_text_field((string) $config['api_login_id']) : '';
1054 $row['transaction_key'] = isset($config['transaction_key']) ? sanitize_text_field((string) $config['transaction_key']) : '';
1055 $row['public_client_key'] = isset($config['public_client_key']) ? sanitize_text_field((string) $config['public_client_key']) : '';
1056 }
1057
1058 if ($sanitized_gateway === 'bank_transfer') {
1059 $row['bank_name'] = isset($config['bank_name']) ? sanitize_text_field((string) $config['bank_name']) : '';
1060 $row['account_name'] = isset($config['account_name']) ? sanitize_text_field((string) $config['account_name']) : '';
1061 $row['account_number'] = isset($config['account_number']) ? sanitize_text_field((string) $config['account_number']) : '';
1062 $row['routing_code'] = isset($config['routing_code']) ? sanitize_text_field((string) $config['routing_code']) : '';
1063 $row['instructions'] = isset($config['instructions']) ? sanitize_textarea_field((string) $config['instructions']) : '';
1064 }
1065
1066 /**
1067 * Allow Pro add-ons or custom code to append keys after core sanitization.
1068 *
1069 * @param array<string, mixed> $row
1070 * @param array<string, mixed> $config
1071 * @return array<string, mixed>
1072 */
1073 $row = apply_filters('yatra_sanitize_gateway_config_row', $row, $sanitized_gateway, $config);
1074
1075 $sanitized[$sanitized_gateway] = $row;
1076 }
1077 return $sanitized;
1078 }
1079
1080 /**
1081 * Normalize multiselect gateway options (Mollie methods, Paystack channels, etc.).
1082 *
1083 * @param mixed $input
1084 * @param array<int, string> $allowed
1085 * @param array<int, string> $default
1086 * @return array<int, string>
1087 */
1088 private function sanitizeGatewayStringList($input, array $allowed, array $default): array
1089 {
1090 if (is_string($input) && $input !== '') {
1091 $input = array_map('trim', explode(',', $input));
1092 }
1093 if (!is_array($input)) {
1094 return $default;
1095 }
1096 $out = [];
1097 foreach ($input as $v) {
1098 $slug = sanitize_key((string) $v);
1099 if ($slug !== '' && in_array($slug, $allowed, true)) {
1100 $out[] = $slug;
1101 }
1102 }
1103 $out = array_values(array_unique($out));
1104
1105 return $out !== [] ? $out : $default;
1106 }
1107
1108 /**
1109 * Sanitize tax rates
1110 */
1111 private function sanitize_tax_rates(array $rates): array
1112 {
1113 $sanitized = [];
1114 foreach ($rates as $country => $rate) {
1115 $sanitized_country = sanitize_text_field($country);
1116 if (is_numeric($rate)) {
1117 $float_rate = (float) $rate;
1118 if ($float_rate >= 0 && $float_rate <= 100) {
1119 $sanitized[$sanitized_country] = $float_rate;
1120 }
1121 }
1122 }
1123 return $sanitized;
1124 }
1125
1126 /**
1127 * Sanitize booking form configuration
1128 */
1129 private function sanitize_booking_form_config(array $config): array
1130 {
1131 $sanitized = [];
1132 $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
1136 foreach ($config as $form_type => $form_config) {
1137 if (!in_array($form_type, $allowed_form_types, true)) {
1138 continue;
1139 }
1140
1141 $sanitized[$form_type] = [
1142 'title' => isset($form_config['title']) ? sanitize_text_field($form_config['title']) : '',
1143 'description' => isset($form_config['description']) ? sanitize_text_field($form_config['description']) : '',
1144 'enabled' => isset($form_config['enabled']) ? (bool) $form_config['enabled'] : true,
1145 'fields' => [],
1146 ];
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;
1152 }
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 }
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 }
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 }
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
1213 $sanitized[$form_type]['fields'][] = $sanitized_field;
1214 }
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 }
1221 }
1222
1223 return apply_filters('yatra_save_booking_form_config', $sanitized, $config);
1224 }
1225
1226 /**
1227 * Flush rewrite rules
1228 */
1229 public function flush_rewrite_rules(WP_REST_Request $request)
1230 {
1231 try {
1232 // Flush rewrite rules
1233 flush_rewrite_rules(true);
1234
1235 return $this->success_response([
1236 'message' => 'Rewrite rules flushed successfully',
1237 ]);
1238 } catch (\Exception $e) {
1239 return $this->error_response($e->getMessage(), 500);
1240 }
1241 }
1242
1243 /**
1244 * Get list of WordPress pages for booking page selection
1245 * Note: We don't check for shortcode here - it's checked on-demand when user selects a page
1246 */
1247 public function get_pages(WP_REST_Request $request)
1248 {
1249 try {
1250 $pages = get_pages([
1251 'post_status' => 'publish',
1252 'sort_column' => 'post_title',
1253 'sort_order' => 'ASC',
1254 ]);
1255
1256 $page_list = [];
1257 foreach ($pages as $page) {
1258 $page_list[] = [
1259 'id' => $page->ID,
1260 'title' => $page->post_title,
1261 'slug' => $page->post_name,
1262 'url' => get_permalink($page->ID),
1263 ];
1264 }
1265
1266 return $this->success_response($page_list);
1267 } catch (\Exception $e) {
1268 return $this->error_response($e->getMessage(), 500);
1269 }
1270 }
1271
1272 /**
1273 * Check if a page has the booking shortcode
1274 */
1275 public function check_booking_shortcode(WP_REST_Request $request)
1276 {
1277 try {
1278 $page_id = (int) $request->get_param('page_id');
1279
1280 if ($page_id <= 0) {
1281 return $this->error_response('Invalid page ID', 400);
1282 }
1283
1284 $page = get_post($page_id);
1285
1286 if (!$page || $page->post_type !== 'page') {
1287 return $this->error_response('Page not found', 404);
1288 }
1289
1290 $has_shortcode = has_shortcode($page->post_content, 'yatra_booking');
1291
1292 return $this->success_response([
1293 'page_id' => $page_id,
1294 'has_shortcode' => $has_shortcode,
1295 'page_title' => $page->post_title,
1296 'page_url' => get_permalink($page_id),
1297 'edit_url' => get_edit_post_link($page_id, 'raw'),
1298 ]);
1299 } catch (\Exception $e) {
1300 return $this->error_response($e->getMessage(), 500);
1301 }
1302 }
1303
1304 /**
1305 * Insert booking shortcode into a page
1306 */
1307 public function insert_booking_shortcode(WP_REST_Request $request)
1308 {
1309 try {
1310 $page_id = (int) $request->get_param('page_id');
1311
1312 if ($page_id <= 0) {
1313 return $this->error_response('Invalid page ID', 400);
1314 }
1315
1316 $page = get_post($page_id);
1317
1318 if (!$page || $page->post_type !== 'page') {
1319 return $this->error_response('Page not found', 404);
1320 }
1321
1322 // Check if shortcode already exists
1323 if (has_shortcode($page->post_content, 'yatra_booking')) {
1324 return $this->success_response([
1325 'message' => 'Shortcode already exists on this page',
1326 'page_id' => $page_id,
1327 'already_exists' => true,
1328 ]);
1329 }
1330
1331 // Append shortcode to page content
1332 $new_content = $page->post_content . "\n\n[yatra_booking]";
1333
1334 $result = wp_update_post([
1335 'ID' => $page_id,
1336 'post_content' => $new_content,
1337 ], true);
1338
1339 if (is_wp_error($result)) {
1340 return $this->error_response($result->get_error_message(), 500);
1341 }
1342
1343 return $this->success_response([
1344 'message' => 'Shortcode added successfully',
1345 'page_id' => $page_id,
1346 'page_url' => get_permalink($page_id),
1347 ]);
1348 } catch (\Exception $e) {
1349 return $this->error_response($e->getMessage(), 500);
1350 }
1351 }
1352
1353 /**
1354 * Keep Settings → Customer "account page" path aligned with {@see RouteMatcher} / {@see Router} (yatra_account_base).
1355 *
1356 * @param array<string, mixed> $settings
1357 * @return array<string, mixed>
1358 */
1359 private function syncAccountRouteSettingsForResponse(array $settings): array
1360 {
1361 // Prefer the full saved path so admin "View" matches Settings → Customer (not only yatra_account_base slug).
1362 $savedPath = get_option('yatra_customer_account_page', '');
1363 if (is_string($savedPath) && $savedPath !== '' && $savedPath !== '0') {
1364 $normalized = '/' . trim(str_replace('\\', '/', $savedPath), '/');
1365 if ($normalized === '/') {
1366 $normalized = '/my-account';
1367 }
1368 $settings['customer_account_page'] = $normalized;
1369
1370 return $settings;
1371 }
1372
1373 $stored = get_option('yatra_account_base', '');
1374 if (is_string($stored) && $stored !== '') {
1375 $settings['customer_account_page'] = '/' . $stored;
1376
1377 return $settings;
1378 }
1379
1380 $cpp = (string) ($settings['customer_account_page'] ?? '');
1381 $slug = self::accountSlugFromCustomerAccountPath($cpp !== '' ? $cpp : '/account');
1382 update_option('yatra_account_base', $slug);
1383 $settings['customer_account_page'] = '/' . $slug;
1384
1385 return $settings;
1386 }
1387
1388 private function persistAccountBaseFromCustomerAccountPage(): void
1389 {
1390 $cpp = (string) get_option('yatra_customer_account_page', '');
1391 update_option('yatra_account_base', self::accountSlugFromCustomerAccountPath($cpp));
1392 }
1393
1394 private static function accountSlugFromCustomerAccountPath(string $path): string
1395 {
1396 $path = trim(str_replace('\\', '/', $path), '/');
1397 $parts = array_values(array_filter(explode('/', $path), static fn ($p) => $p !== ''));
1398 $segment = $parts !== [] ? end($parts) : 'account';
1399 $slug = sanitize_title($segment);
1400
1401 return $slug !== '' ? $slug : 'account';
1402 }
1403 }
1404
1405