PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.13
Yatra – Travel Booking & Tour Operator Software v3.0.13
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 3.0.13, at app/Controllers/SettingsController.php

1,378 lines 58.4 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 'allow_save_payment_methods' => false,
107
108 // Email Settings (WordPress site defaults when Yatra options are missing)
109 'admin_email' => $wpAdminEmail,
110 'from_email' => $wpAdminEmail,
111 'from_name' => $wpSiteName,
112 // Blind copy of every outgoing Yatra email, for archiving/monitoring.
113 // Empty means no copy is sent; accepts several comma-separated addresses.
114 'email_always_bcc' => '',
115 'email_template_booking' => true,
116 'email_template_confirmation' => true,
117 // Separate part-payment email. Off by default so existing sites keep
118 // sending the single payment template for every payment.
119 'email_template_partial_payment' => false,
120 'email_template_cancellation' => true,
121 'email_template_reminder' => true,
122 'email_template_admin_new_booking' => true,
123 'email_template_admin_payment' => true,
124 'email_template_admin_cancellation' => true,
125 'email_template_trip_consent' => true,
126 'email_template_customer_verification' => true,
127 'email_template_guest_verification' => true,
128 'email_template_account_email_change' => true,
129 'email_template_account_email_changed' => true,
130 'email_template_booking_completed' => true,
131 'email_template_booking_expired_customer' => true,
132 'email_template_admin_booking_expired' => true,
133 'email_template_scheduled_payment_reminder' => true,
134 'email_template_scheduled_payment_succeeded' => true,
135 'email_template_scheduled_payment_failed' => true,
136 'email_template_admin_scheduled_payment_failed' => true,
137 'email_template_enquiry_received' => true,
138 'email_template_enquiry_admin' => true,
139 'email_template_enquiry_response' => true,
140 'email_template_review_request' => true,
141 'email_template_abandoned_booking_recovery_first' => true,
142 'email_template_abandoned_booking_recovery_second' => true,
143 'email_template_abandoned_booking_recovery_final' => true,
144 'smtp_enabled' => false,
145 'smtp_host' => 'smtp.gmail.com',
146 'smtp_port' => 587,
147 'smtp_username' => '',
148 'smtp_password' => '',
149 'smtp_encryption' => 'tls',
150
151 // Customer Settings
152 'customer_registration' => true,
153 'customer_fields' => [],
154 'require_email_verification' => false,
155 // Per-booking verification for guest checkouts. Distinct from the
156 // account-creation `require_email_verification` flag because a guest
157 // never registers — the verification is gated on the booking itself
158 // (BookingSessionController checks this when admitting a guest).
159 'require_guest_email_verification' => false,
160 'customer_account_page' => '',
161 'allow_customer_reviews' => true,
162 'customer_dashboard_enabled' => true,
163
164 // Review Settings
165 'enable_reviews' => true,
166 'require_booking' => true,
167 'auto_approve_reviews' => false,
168 'review_moderation' => true,
169 'min_rating' => 1,
170 'allow_anonymous_reviews' => false,
171 'review_reminder_days' => 7,
172
173 // Tax Settings
174 'enable_tax' => false,
175 'tax_name' => __('Tax', 'yatra'),
176 'tax_rate' => 0,
177 'tax_inclusive' => false,
178 'vat_number' => '',
179 'tax_by_country' => false,
180 'tax_rates' => [],
181 'multiple_taxes_enabled' => false,
182 'multiple_taxes' => [],
183 'multiple_taxes_by_country' => [],
184
185 // Currency Settings
186 'default_currency' => 'USD',
187 'multi_currency' => false,
188 'currency_position' => 'left',
189 'currency_decimals' => 2,
190 'decimal_separator'=>'.',
191 'thousand_separator'=>',',
192
193 // Notification Settings (SMS / future channels — booking email toggles live under Email → Templates)
194 'sms_notifications' => false,
195 'sms_provider' => '',
196 'sms_api_key' => '',
197
198 // Integration Settings
199 'google_analytics' => '',
200 'facebook_pixel' => '',
201 'recaptcha_enabled' => false,
202 'recaptcha_site_key' => '',
203 'recaptcha_secret_key' => '',
204 // reCAPTCHA v3: score threshold (0.0-1.0) + per-form protection toggles.
205 // All off by default so enabling reCAPTCHA alone changes nothing until
206 // the operator picks which forms to protect.
207 'recaptcha_score_threshold' => 0.5,
208 'recaptcha_protect_enquiry' => false,
209 'recaptcha_protect_booking' => false,
210 'recaptcha_protect_registration' => false,
211
212 // Permalink Settings
213 'trip_base' => 'trip',
214 'destination_base' => 'destination',
215 'activity_base' => 'activity',
216 'trip_category_base' => 'trip-category',
217 'booking_base' => 'book',
218 // Wishlist (Pro) — stored in free options; active only when Pro + setting on
219 'enable_wishlist' => false,
220 // Sold-out date visibility on the storefront. Default true keeps the
221 // existing behaviour (sold-out dates stay visible, badged "sold out" and
222 // able to drive the waitlist); owners can switch it off to hide them the
223 // same way blocked dates are hidden.
224 'show_sold_out' => true,
225
226 // Search & Listing storefront UX. Defaults preserve current behaviour:
227 // every search field shown (true) and mobile filters expanded (false),
228 // so existing installs are unchanged until the owner opts in. Booleans
229 // are auto-sanitized from the default type.
230 'search_show_keyword' => true,
231 'search_show_destination' => true,
232 'search_show_activities' => true,
233 'search_show_duration' => true,
234 'search_show_budget' => true,
235 'collapse_filters_on_mobile' => false,
236
237 // Booking Page Settings
238 'use_booking_page' => false,
239 'booking_page_id' => 0,
240
241 // Legal Pages (Booking UI)
242 'terms_page_id' => 0,
243 'privacy_policy_page_id' => 0,
244
245 // SEO Settings
246 'seo_trip_meta_title' => '',
247 'seo_trip_meta_description' => '',
248 'seo_trip_meta_keywords' => '',
249 'seo_trip_meta_image' => 0,
250 'enable_sitemap' => true,
251
252 // Advanced Settings
253 'debug_mode' => false,
254 'enable_logging' => false,
255 'cache_enabled' => true,
256 'api_key' => '',
257 'api_rate_limit' => 100,
258 'session_timeout' => 3600,
259
260 // Booking Form Builder
261 'booking_form_config' => [],
262 ];
263
264 $base_settings = array_merge(
265 $base_settings,
266 \Yatra\Services\EmailTemplateDefaults::settingsOptionDefaults()
267 );
268
269 // Allow Pro plugins to add their settings via filter
270 $this->default_settings = apply_filters('yatra_settings_default_fields', $base_settings);
271 }
272
273 public function register_routes(): void
274 {
275 $namespace = 'yatra/v1';
276 $base = 'settings';
277
278 register_rest_route($namespace, '/' . $base, [
279 [
280 'methods' => \WP_REST_Server::READABLE,
281 'callback' => [$this, 'get_settings'],
282 'permission_callback' => [$this, 'check_permission'],
283 ],
284 [
285 'methods' => \WP_REST_Server::EDITABLE,
286 'callback' => [$this, 'update_settings'],
287 'permission_callback' => [$this, 'check_permission'],
288 ],
289 ]);
290
291 // Flush rewrite rules endpoint
292 register_rest_route($namespace, '/' . $base . '/flush-rewrite-rules', [
293 [
294 'methods' => \WP_REST_Server::CREATABLE,
295 'callback' => [$this, 'flush_rewrite_rules'],
296 'permission_callback' => [$this, 'check_permission'],
297 ],
298 ]);
299
300 // Get WordPress pages for booking page selection
301 register_rest_route($namespace, '/' . $base . '/pages', [
302 [
303 'methods' => \WP_REST_Server::READABLE,
304 'callback' => [$this, 'get_pages'],
305 'permission_callback' => [$this, 'check_permission'],
306 ],
307 ]);
308
309 // Check if page has booking shortcode
310 register_rest_route($namespace, '/' . $base . '/check-shortcode/(?P<page_id>\d+)', [
311 [
312 'methods' => \WP_REST_Server::READABLE,
313 'callback' => [$this, 'check_booking_shortcode'],
314 'permission_callback' => [$this, 'check_permission'],
315 ],
316 ]);
317
318 // Insert booking shortcode into page
319 register_rest_route($namespace, '/' . $base . '/insert-shortcode/(?P<page_id>\d+)', [
320 [
321 'methods' => \WP_REST_Server::CREATABLE,
322 'callback' => [$this, 'insert_booking_shortcode'],
323 'permission_callback' => [$this, 'check_permission'],
324 ],
325 ]);
326
327 register_rest_route($namespace, '/' . $base . '/email-template-preview', [
328 [
329 'methods' => \WP_REST_Server::CREATABLE,
330 'callback' => [$this, 'preview_core_email_template'],
331 'permission_callback' => [$this, 'check_permission'],
332 ],
333 ]);
334 }
335
336 /**
337 * Preview a core (settings-backed) transactional template with sample merge data.
338 */
339 public function preview_core_email_template(WP_REST_Request $request)
340 {
341 try {
342 $params = $request->get_json_params();
343 if (!is_array($params)) {
344 return $this->error_response(__('Invalid request body.', 'yatra'), 400);
345 }
346
347 $templateKey = sanitize_key($params['template_key'] ?? '');
348 $subjectTpl = sanitize_text_field($params['subject'] ?? '');
349 $bodyTpl = wp_kses_post($params['body'] ?? '');
350 $tripId = isset($params['trip_id']) ? (int) $params['trip_id'] : 0;
351 $tripId = $tripId > 0 ? $tripId : null;
352
353 $rendered = EmailTemplatePreviewService::render($templateKey, $subjectTpl, $bodyTpl, $tripId);
354
355 return $this->success_response([
356 'success' => true,
357 'data' => [
358 'subject' => $rendered['subject'],
359 'body' => $rendered['body'],
360 ],
361 ]);
362 } catch (\InvalidArgumentException $e) {
363 return $this->error_response($e->getMessage(), 400);
364 } catch (\Exception $e) {
365 return $this->error_response($e->getMessage(), 500);
366 }
367 }
368
369 /**
370 * Plugin settings — high-sensitivity cap. By default only the
371 * Owner role holds `yatra_manage_settings` (Manager doesn't, by
372 * design — settings include payment gateway routing, email
373 * delivery configuration, currency formatting and similar
374 * global behaviour). WP admins pass via the Team module's
375 * admin-fallback filter.
376 */
377 public function check_permission(?WP_REST_Request $request = null): bool
378 {
379 if (!is_user_logged_in()) {
380 return false;
381 }
382 return current_user_can('yatra_manage_settings');
383 }
384
385 /**
386 * Get all settings
387 */
388 public function get_settings(WP_REST_Request $request)
389 {
390 try {
391 $settings = [];
392
393 // Get all settings from WordPress options table with yatra_ prefix
394 foreach ($this->default_settings as $key => $default_value) {
395 $option_name = 'yatra_' . $key;
396 $value = get_option($option_name, false);
397
398 // Only use default if option doesn't exist (wasn't set by InstallerService)
399 if ($value === false) {
400 $value = $default_value;
401 }
402
403 // Stored empty string should behave like "unset" for delivery identity (matches installer / backfill).
404 if (($key === 'admin_email' || $key === 'from_email') && is_string($value) && trim($value) === '') {
405 $wp = (string) get_option('admin_email', '');
406 $value = $wp !== '' ? $wp : $value;
407 }
408 if ($key === 'from_name' && is_string($value) && trim($value) === '') {
409 $wp = (string) get_bloginfo('name');
410 $value = $wp !== '' ? $wp : $value;
411 }
412
413 // Handle serialized arrays (for fields like payment_gateways, customer_fields, etc.)
414 if (is_string($value) && is_serialized($value)) {
415 $value = maybe_unserialize($value);
416 }
417
418 // Ensure arrays are returned as arrays (not objects)
419 if (is_array($default_value) && !is_array($value)) {
420 $value = [];
421 }
422
423 $settings[$key] = $value;
424 }
425
426 // Special handling for booking_form_config - always use getBookingFormConfig which handles locked fields
427 $settings['booking_form_config'] = \Yatra\Services\SettingsService::getBookingFormConfig();
428
429 // Merge in flexible payment settings from Pro module if enabled
430 $flexible_payment_settings = apply_filters('yatra_get_flexible_payment_settings', []);
431 if (!empty($flexible_payment_settings)) {
432 $settings = array_merge($settings, $flexible_payment_settings);
433 }
434
435 $scheduled_payment_settings = apply_filters('yatra_get_scheduled_payment_settings', []);
436 if (!empty($scheduled_payment_settings)) {
437 $settings = array_merge($settings, $scheduled_payment_settings);
438 }
439
440 // Scheduled payment keys are owned by Pro (yatra_pro_scheduled_payments), not yatra_* options.
441 foreach (
442 [
443 'enable_scheduled_payments',
444 'scheduled_payment_type',
445 'scheduled_payment_days',
446 'scheduled_payment_installments',
447 'scheduled_payment_interval',
448 'scheduled_payment_reminder_days',
449 ] as $sk
450 ) {
451 if (array_key_exists($sk, $this->default_settings)) {
452 $settings[$sk] = \Yatra\Services\SettingsService::get(
453 $sk,
454 $this->default_settings[$sk]
455 );
456 }
457 }
458
459 $settings = $this->syncAccountRouteSettingsForResponse($settings);
460
461 /**
462 * Allow Pro modules to align REST payloads with canonical option stores
463 * (e.g. GA4 settings that also live in yatra_google_analytics_settings).
464 */
465 $settings = apply_filters('yatra_rest_settings', $settings);
466
467 return $this->success_response($settings);
468 } catch (\Exception $e) {
469 return $this->error_response($e->getMessage(), 500);
470 }
471 }
472
473 /**
474 * Update settings
475 */
476 public function update_settings(WP_REST_Request $request)
477 {
478 try {
479 $data = $request->get_json_params();
480
481 if (!is_array($data)) {
482 return $this->error_response('Invalid settings data', 400);
483 }
484
485 $updated = [];
486 $errors = [];
487
488 // Check if Dynamic Form Field module is enabled
489 $is_dynamic_form_enabled = apply_filters('yatra_dynamic_form_field_enabled', false);
490
491 // Check if Flexible Payments module is enabled (Pro feature)
492 $is_flexible_payments_enabled = apply_filters('yatra_flexible_payments_enabled', false);
493
494 $is_scheduled_payments_module = apply_filters('yatra_scheduled_payments_module_active', false);
495
496 // Flexible payment settings keys (Pro only)
497 $flexible_payment_keys = [
498 'deposit_required', 'deposit_percentage', 'partial_payment',
499 'partial_payment_percentage', 'enable_deposit', 'allow_save_payment_methods',
500 ];
501
502 $scheduled_payment_keys = [
503 'enable_scheduled_payments',
504 'scheduled_payment_type',
505 'scheduled_payment_days',
506 'scheduled_payment_installments',
507 'scheduled_payment_interval',
508 'scheduled_payment_reminder_days',
509 ];
510
511 // Collect flexible payment settings to delegate to Pro
512 $flexible_payment_settings = [];
513
514 $scheduled_payment_settings_batch = [];
515
516 // Process each setting
517 foreach ($data as $key => $value) {
518 // Skip booking_form_config if Dynamic Form Field module is not enabled
519 // This allows the settings to save without error when the module is disabled
520 if ($key === 'booking_form_config' && !$is_dynamic_form_enabled) {
521 continue;
522 }
523
524 // Delegate flexible payment settings to Pro module
525 if (in_array($key, $flexible_payment_keys, true)) {
526 if ($is_flexible_payments_enabled) {
527 $flexible_payment_settings[$key] = $value;
528 }
529 // Skip saving in Free plugin - Pro handles these
530 continue;
531 }
532
533 if (in_array($key, $scheduled_payment_keys, true)) {
534 if ($is_scheduled_payments_module) {
535 $scheduled_payment_settings_batch[$key] = $value;
536 }
537 continue;
538 }
539
540 // Wishlist toggle: only meaningful with Yatra Pro active
541 if ($key === 'enable_wishlist' && !apply_filters('yatra_is_pro_active', false)) {
542 continue;
543 }
544
545 // Validate that the key exists in default settings
546 if (!array_key_exists($key, $this->default_settings)) {
547 $errors[] = sprintf('Unknown setting: %s', $key);
548 continue;
549 }
550
551 // Sanitize and validate the value based on its type
552 $sanitized_value = $this->sanitize_setting($key, $value);
553
554 if ($sanitized_value === null) {
555 $errors[] = sprintf('Invalid value for setting: %s', $key);
556 continue;
557 }
558
559 // Save to WordPress options table with yatra_ prefix
560 $option_name = 'yatra_' . $key;
561
562 // Serialize arrays for storage
563 if (is_array($sanitized_value)) {
564 $sanitized_value = maybe_serialize($sanitized_value);
565 }
566
567 $result = update_option($option_name, $sanitized_value);
568
569 if ($result !== false) {
570 $updated[] = $key;
571 }
572 }
573
574 // Delegate flexible payment settings to Pro module for saving
575 if (!empty($flexible_payment_settings) && $is_flexible_payments_enabled) {
576 do_action('yatra_save_flexible_payment_settings', $flexible_payment_settings);
577 $updated = array_merge($updated, array_keys($flexible_payment_settings));
578 }
579
580 if (!empty($scheduled_payment_settings_batch) && $is_scheduled_payments_module) {
581 do_action('yatra_save_scheduled_payment_settings', $scheduled_payment_settings_batch);
582 $updated = array_merge($updated, array_keys($scheduled_payment_settings_batch));
583 }
584
585 // Sync currency keys: keep 'currency' and 'default_currency' in sync
586 // Admin UI has both Payment Settings (currency) and Currency Settings (default_currency)
587 if (in_array('default_currency', $updated, true) && !in_array('currency', $updated, true)) {
588 $sync_currency = get_option('yatra_default_currency', 'USD');
589 update_option('yatra_currency', $sync_currency);
590 } elseif (in_array('currency', $updated, true) && !in_array('default_currency', $updated, true)) {
591 $sync_currency = get_option('yatra_currency', 'USD');
592 update_option('yatra_default_currency', $sync_currency);
593 }
594
595 if (in_array('customer_account_page', $updated, true)) {
596 $this->persistAccountBaseFromCustomerAccountPage();
597 }
598
599 if (!empty($errors)) {
600 $errorSummary = implode('; ', $errors);
601 return $this->error_response(
602 sprintf('Some settings could not be updated: %s', $errorSummary),
603 400,
604 [
605 'errors' => $errors,
606 'updated' => $updated,
607 ]
608 );
609 }
610
611 // Flush rewrite rules if permalink settings were updated
612 if (in_array('trip_base', $updated, true) ||
613 in_array('destination_base', $updated, true) ||
614 in_array('activity_base', $updated, true) ||
615 in_array('trip_category_base', $updated, true) ||
616 in_array('booking_base', $updated, true) ||
617 in_array('use_booking_page', $updated, true) ||
618 in_array('booking_page_id', $updated, true) ||
619 in_array('customer_account_page', $updated, true)) {
620 // Use hard flush to ensure rules are saved to database
621 flush_rewrite_rules(true);
622 }
623
624 if (!empty($updated)) {
625 \Yatra\Services\SettingsService::reload();
626 }
627
628 // Cross-validation: booking-auth settings interact via OR
629 // logic in booking-content.php, so some combinations are
630 // semantically inconsistent or redundant. We don't block
631 // the save (the resulting state still has well-defined
632 // behavior), but we surface a clear notice so the operator
633 // understands what they just configured.
634 //
635 // require_login=true + allow_guest_checkout=true →
636 // require_login wins; allow_guest_checkout is a no-op.
637 // require_login=true + allow_guest_checkout=false →
638 // Strictest setting (login required, no guest path).
639 // Internally consistent.
640 // require_login=false + allow_guest_checkout=false →
641 // Guests blocked, logged-in users can book. Consistent.
642 // require_login=false + allow_guest_checkout=true →
643 // Default. Permissive.
644 $notices = [];
645 $effective_require_login = \array_key_exists('require_login', $data)
646 ? (bool) $data['require_login']
647 : (bool) \Yatra\Services\SettingsService::get('require_login', false);
648 $effective_allow_guest = \array_key_exists('allow_guest_checkout', $data)
649 ? (bool) $data['allow_guest_checkout']
650 : (bool) \Yatra\Services\SettingsService::get('allow_guest_checkout', true);
651
652 if ($effective_require_login && $effective_allow_guest) {
653 $notices[] = [
654 'level' => 'warning',
655 'code' => 'booking_auth_redundant',
656 'message' => __(
657 '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.',
658 'yatra'
659 ),
660 ];
661 }
662
663 // Scheduled Payments + guest checkout — incompatible at
664 // the gateway level. Scheduled charges require a saved
665 // payment-method tied to a customer record on the
666 // gateway side (Stripe Customer, etc.), which in turn
667 // requires a logged-in WP user. When both settings are
668 // on, the system gracefully skips installment creation
669 // for guest bookings — but operators expect them to
670 // work and only discover the gap when reconciling
671 // unpaid bookings weeks later. Surface this proactively.
672 $effective_scheduled_payments = \array_key_exists('enable_scheduled_payments', $data)
673 ? (bool) $data['enable_scheduled_payments']
674 : (bool) \Yatra\Services\SettingsService::get('enable_scheduled_payments', false);
675 if (
676 $effective_scheduled_payments
677 && $effective_allow_guest
678 && !$effective_require_login
679 ) {
680 $notices[] = [
681 'level' => 'info',
682 'code' => 'scheduled_payments_guest_caveat',
683 'message' => __(
684 '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.',
685 'yatra'
686 ),
687 ];
688 }
689
690 $response = [
691 'message' => 'Settings updated successfully',
692 'updated' => $updated,
693 ];
694 if ($notices !== []) {
695 $response['notices'] = $notices;
696 }
697 return $this->success_response($response);
698 } catch (\Exception $e) {
699 return $this->error_response($e->getMessage(), 500);
700 }
701 }
702
703 /**
704 * Sanitize and validate setting value
705 * Pro plugins can handle sanitization of their own settings via filter
706 *
707 * @param mixed $value
708 * @return mixed
709 */
710 private function sanitize_setting(string $key, $value)
711 {
712 $default = $this->default_settings[$key] ?? null;
713 $default_type = gettype($default);
714
715 // Allow Pro plugins to handle sanitization of their own settings
716 $filtered_value = apply_filters('yatra_sanitize_setting', null, $key, $value, $default);
717 if ($filtered_value !== null) {
718 return $filtered_value;
719 }
720
721 // Handle null values - use default
722 if ($value === null) {
723 return $default;
724 }
725
726 // Handle arrays
727 if (is_array($default)) {
728 if (!is_array($value)) {
729 return null;
730 }
731 // Sanitize array values
732 return array_map(function($item) {
733 if (is_string($item)) {
734 return sanitize_text_field($item);
735 }
736 if (is_numeric($item)) {
737 return is_float($item) ? (float) $item : (int) $item;
738 }
739 if (is_bool($item)) {
740 return (bool) $item;
741 }
742 if (is_array($item)) {
743 return $this->sanitize_array($item);
744 }
745 return $item;
746 }, $value);
747 }
748
749 // Handle booleans (REST may send true/false strings)
750 if (is_bool($default)) {
751 if (is_bool($value)) {
752 return $value;
753 }
754 if (is_string($value)) {
755 $parsed = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
756 return $parsed !== null ? $parsed : (bool) $value;
757 }
758 return (bool) $value;
759 }
760
761 // Handle integers
762 if (is_int($default)) {
763 if (!is_numeric($value)) {
764 return null;
765 }
766 $int_value = (int) $value;
767 // Validate ranges for specific fields
768 if ($key === 'booking_expiry_hours' && $int_value < 0) {
769 return null;
770 }
771 if ($key === 'partial_payment_percentage' && ($int_value < 0 || $int_value > 100)) {
772 return null;
773 }
774 if ($key === 'deposit_percentage' && ($int_value < 0 || $int_value > 100)) {
775 return null;
776 }
777 if ($key === 'tax_rate' && ($int_value < 0 || $int_value > 100)) {
778 return null;
779 }
780 if ($key === 'smtp_port' && ($int_value < 1 || $int_value > 65535)) {
781 return null;
782 }
783 return $int_value;
784 }
785
786 // Handle floats
787 if (is_float($default)) {
788 if (!is_numeric($value)) {
789 return null;
790 }
791 $float_value = (float) $value;
792 if ($float_value < 0) {
793 return null;
794 }
795 return $float_value;
796 }
797
798 // Handle strings
799 if (is_string($default)) {
800 if ($key === 'timezone') {
801 $tz = is_string($value) ? trim($value) : '';
802 if ($tz === '') {
803 return is_string($default) ? $default : 'UTC';
804 }
805 try {
806 new \DateTimeZone($tz);
807
808 return $tz;
809 } catch (\Exception $e) {
810 return is_string($default) ? $default : 'UTC';
811 }
812 }
813 if ($key === 'currency_position') {
814 $allowed = ['left', 'right', 'left_space', 'right_space', 'before', 'after'];
815 $v = is_string($value) ? strtolower(trim($value)) : '';
816
817 return in_array($v, $allowed, true) ? $v : (is_string($default) ? $default : 'left');
818 }
819 if ($key === 'discount_stacking_mode') {
820 // Strict enum — any other value silently falls back to the
821 // backward-compatible default so a malformed POST cannot
822 // change pricing behavior unexpectedly.
823 $allowed = ['both', 'discount_only', 'dynamic_pricing_only', 'best_for_customer'];
824 $v = is_string($value) ? strtolower(trim($value)) : '';
825
826 return in_array($v, $allowed, true) ? $v : 'both';
827 }
828 // Special handling for specific fields
829 if ($key === 'company_email' || $key === 'admin_email' || $key === 'from_email' || $key === 'smtp_username') {
830 return sanitize_email($value);
831 }
832 if ($key === 'company_website' || $key === 'company_logo' || $key === 'google_analytics' || $key === 'facebook_pixel') {
833 return esc_url_raw($value);
834 }
835 if ($key === 'seo_trip_meta_title') {
836 // Allow more characters for meta title, but strip HTML
837 return wp_strip_all_tags($value);
838 }
839 if ($key === 'seo_trip_meta_description') {
840 // Allow more characters for meta description, but strip HTML
841 return wp_strip_all_tags($value);
842 }
843 if ($key === 'seo_trip_meta_keywords') {
844 // Allow keywords, strip HTML and sanitize
845 return sanitize_text_field($value);
846 }
847 if ($key === 'frontend_primary_color') {
848 return \Yatra\Utils\FrontendThemeCss::sanitizePrimaryColor(is_string($value) ? $value : '');
849 }
850 if ($key === 'frontend_container_max_width') {
851 return \Yatra\Utils\FrontendThemeCss::sanitizeContainerMaxWidthSetting(
852 is_string($value) ? $value : ''
853 );
854 }
855 if (is_string($key) && strpos($key, 'email_tpl_') === 0 && substr($key, -5) === '_body') {
856 return wp_kses_post((string) $value);
857 }
858 if (is_string($key) && strpos($key, 'email_tpl_') === 0 && substr($key, -8) === '_subject') {
859 return sanitize_text_field((string) $value);
860 }
861 if ($key === 'smtp_password' || $key === 'api_key' || $key === 'sms_api_key' || $key === 'recaptcha_secret_key') {
862 // Don't sanitize passwords/keys too aggressively
863 return sanitize_text_field($value);
864 }
865 if ($key === 'gateway_configs') {
866 // Handle nested array structure for gateway configs
867 if (is_array($value)) {
868 return $this->sanitize_gateway_configs($value);
869 }
870 return [];
871 }
872 if ($key === 'booking_form_config') {
873 // Handle nested array structure for booking form config
874 if (is_array($value)) {
875 return $this->sanitize_booking_form_config($value);
876 }
877 return [];
878 }
879 if ($key === 'tax_rates') {
880 // Handle nested array structure for tax rates
881 if (is_array($value)) {
882 return $this->sanitize_tax_rates($value);
883 }
884 return [];
885 }
886 return sanitize_text_field($value);
887 }
888
889 return $value;
890 }
891
892 /**
893 * Sanitize nested array
894 */
895 private function sanitize_array(array $array): array
896 {
897 $sanitized = [];
898 foreach ($array as $k => $v) {
899 $sanitized_key = is_string($k) ? sanitize_key($k) : $k;
900 if (is_array($v)) {
901 $sanitized[$sanitized_key] = $this->sanitize_array($v);
902 } elseif (is_string($v)) {
903 $sanitized[$sanitized_key] = sanitize_text_field($v);
904 } elseif (is_numeric($v)) {
905 $sanitized[$sanitized_key] = is_float($v) ? (float) $v : (int) $v;
906 } elseif (is_bool($v)) {
907 $sanitized[$sanitized_key] = (bool) $v;
908 } else {
909 $sanitized[$sanitized_key] = $v;
910 }
911 }
912 return $sanitized;
913 }
914
915 /**
916 * Sanitize gateway configs
917 */
918 private function sanitize_gateway_configs(array $configs): array
919 {
920 $sanitized = [];
921 foreach ($configs as $gateway => $config) {
922 if (!is_array($config)) {
923 continue;
924 }
925 $sanitized_gateway = sanitize_key($gateway);
926 $row = [
927 'enabled' => isset($config['enabled']) ? (bool) $config['enabled'] : false,
928 'icon' => isset($config['icon']) ? esc_url_raw($config['icon']) : '',
929 'title' => isset($config['title']) ? sanitize_text_field($config['title']) : '',
930 'description' => isset($config['description']) ? sanitize_textarea_field($config['description']) : '',
931 'api_key' => isset($config['api_key']) ? sanitize_text_field($config['api_key']) : '',
932 'api_secret' => isset($config['api_secret']) ? sanitize_text_field($config['api_secret']) : '',
933 'client_id' => isset($config['client_id']) ? sanitize_text_field($config['client_id']) : '',
934 'client_secret' => isset($config['client_secret']) ? sanitize_text_field($config['client_secret']) : '',
935 'merchant_id' => isset($config['merchant_id']) ? sanitize_text_field($config['merchant_id']) : '',
936 'public_key' => isset($config['public_key']) ? sanitize_text_field($config['public_key']) : '',
937 'private_key' => isset($config['private_key']) ? sanitize_text_field($config['private_key']) : '',
938 'webhook_secret' => isset($config['webhook_secret']) ? sanitize_text_field($config['webhook_secret']) : '',
939 'test_mode' => isset($config['test_mode']) ? (bool) $config['test_mode'] : false,
940 'sandbox' => isset($config['sandbox']) ? (bool) $config['sandbox'] : false,
941 ];
942
943 if ($sanitized_gateway === 'paypal') {
944 $mode = isset($config['mode']) && in_array((string) $config['mode'], ['simple', 'advanced'], true)
945 ? (string) $config['mode']
946 : 'simple';
947 $row['email'] = isset($config['email']) ? sanitize_email((string) $config['email']) : '';
948 $row['mode'] = $mode;
949 }
950
951 if ($sanitized_gateway === 'pay_later') {
952 $row['payment_deadline_days'] = isset($config['payment_deadline_days'])
953 ? max(1, min(60, (int) $config['payment_deadline_days']))
954 : 7;
955 $row['auto_cancel_days'] = isset($config['auto_cancel_days'])
956 ? max(0, min(30, (int) $config['auto_cancel_days']))
957 : 3;
958 $row['require_deposit'] = isset($config['require_deposit']) ? (bool) $config['require_deposit'] : false;
959 $row['deposit_amount'] = isset($config['deposit_amount'])
960 ? max(1, min(50, (int) $config['deposit_amount']))
961 : 10;
962 $row['reminder_days'] = isset($config['reminder_days'])
963 ? sanitize_text_field((string) $config['reminder_days'])
964 : '7,3,1';
965 }
966
967 if ($sanitized_gateway === 'stripe') {
968 $allowedStripeMethods = ['card', 'google_pay', 'apple_pay'];
969 $methodsRaw = isset($config['enabled_methods']) ? (string) $config['enabled_methods'] : '';
970 if ($methodsRaw !== '') {
971 $parts = array_filter(array_map('trim', explode(',', $methodsRaw)));
972 $normalized = [];
973 foreach ($parts as $part) {
974 $slug = strtolower($part);
975 if (in_array($slug, $allowedStripeMethods, true)) {
976 $normalized[] = $slug;
977 }
978 }
979 $row['enabled_methods'] = $normalized !== [] ? implode(',', $normalized) : 'card,google_pay,apple_pay';
980 } else {
981 $row['enabled_methods'] = 'card,google_pay,apple_pay';
982 }
983 foreach (['live_publishable_key', 'live_secret_key', 'test_publishable_key', 'test_secret_key'] as $stripeKey) {
984 if (array_key_exists($stripeKey, $config)) {
985 $row[$stripeKey] = sanitize_text_field((string) $config[$stripeKey]);
986 }
987 }
988 }
989
990 if ($sanitized_gateway === 'razorpay') {
991 $row['key_id'] = isset($config['key_id']) ? sanitize_text_field((string) $config['key_id']) : '';
992 $row['key_secret'] = isset($config['key_secret']) ? sanitize_text_field((string) $config['key_secret']) : '';
993 }
994
995 if ($sanitized_gateway === 'mollie') {
996 $row['api_key'] = isset($config['api_key']) ? sanitize_text_field((string) $config['api_key']) : '';
997 $row['webhook_url'] = isset($config['webhook_url']) ? esc_url_raw((string) $config['webhook_url']) : '';
998 $allowedMollie = ['creditcard', 'ideal', 'bancontact', 'sofort', 'eps', 'giropay', 'paypal', 'sepadirectdebit'];
999 $row['payment_methods'] = $this->sanitizeGatewayStringList(
1000 $config['payment_methods'] ?? [],
1001 $allowedMollie,
1002 ['creditcard', 'ideal', 'paypal']
1003 );
1004 }
1005
1006 if ($sanitized_gateway === 'paystack') {
1007 $row['public_key'] = isset($config['public_key']) ? sanitize_text_field((string) $config['public_key']) : '';
1008 $row['secret_key'] = isset($config['secret_key']) ? sanitize_text_field((string) $config['secret_key']) : '';
1009 $row['webhook_url'] = isset($config['webhook_url']) ? esc_url_raw((string) $config['webhook_url']) : '';
1010 $allowedPaystack = ['card', 'bank', 'ussd', 'qr', 'mobile_money', 'bank_transfer'];
1011 $row['payment_channels'] = $this->sanitizeGatewayStringList(
1012 $config['payment_channels'] ?? [],
1013 $allowedPaystack,
1014 ['card', 'bank', 'ussd']
1015 );
1016 unset($row['private_key']);
1017 }
1018
1019 if ($sanitized_gateway === 'square') {
1020 $row['application_id'] = isset($config['application_id']) ? sanitize_text_field((string) $config['application_id']) : '';
1021 $row['access_token'] = isset($config['access_token']) ? sanitize_text_field((string) $config['access_token']) : '';
1022 $row['location_id'] = isset($config['location_id']) ? sanitize_text_field((string) $config['location_id']) : '';
1023 }
1024
1025 if ($sanitized_gateway === 'authorize_net') {
1026 $row['api_login_id'] = isset($config['api_login_id']) ? sanitize_text_field((string) $config['api_login_id']) : '';
1027 $row['transaction_key'] = isset($config['transaction_key']) ? sanitize_text_field((string) $config['transaction_key']) : '';
1028 $row['public_client_key'] = isset($config['public_client_key']) ? sanitize_text_field((string) $config['public_client_key']) : '';
1029 }
1030
1031 if ($sanitized_gateway === 'bank_transfer') {
1032 $row['bank_name'] = isset($config['bank_name']) ? sanitize_text_field((string) $config['bank_name']) : '';
1033 $row['account_name'] = isset($config['account_name']) ? sanitize_text_field((string) $config['account_name']) : '';
1034 $row['account_number'] = isset($config['account_number']) ? sanitize_text_field((string) $config['account_number']) : '';
1035 $row['routing_code'] = isset($config['routing_code']) ? sanitize_text_field((string) $config['routing_code']) : '';
1036 $row['instructions'] = isset($config['instructions']) ? sanitize_textarea_field((string) $config['instructions']) : '';
1037 }
1038
1039 /**
1040 * Allow Pro add-ons or custom code to append keys after core sanitization.
1041 *
1042 * @param array<string, mixed> $row
1043 * @param array<string, mixed> $config
1044 * @return array<string, mixed>
1045 */
1046 $row = apply_filters('yatra_sanitize_gateway_config_row', $row, $sanitized_gateway, $config);
1047
1048 $sanitized[$sanitized_gateway] = $row;
1049 }
1050 return $sanitized;
1051 }
1052
1053 /**
1054 * Normalize multiselect gateway options (Mollie methods, Paystack channels, etc.).
1055 *
1056 * @param mixed $input
1057 * @param array<int, string> $allowed
1058 * @param array<int, string> $default
1059 * @return array<int, string>
1060 */
1061 private function sanitizeGatewayStringList($input, array $allowed, array $default): array
1062 {
1063 if (is_string($input) && $input !== '') {
1064 $input = array_map('trim', explode(',', $input));
1065 }
1066 if (!is_array($input)) {
1067 return $default;
1068 }
1069 $out = [];
1070 foreach ($input as $v) {
1071 $slug = sanitize_key((string) $v);
1072 if ($slug !== '' && in_array($slug, $allowed, true)) {
1073 $out[] = $slug;
1074 }
1075 }
1076 $out = array_values(array_unique($out));
1077
1078 return $out !== [] ? $out : $default;
1079 }
1080
1081 /**
1082 * Sanitize tax rates
1083 */
1084 private function sanitize_tax_rates(array $rates): array
1085 {
1086 $sanitized = [];
1087 foreach ($rates as $country => $rate) {
1088 $sanitized_country = sanitize_text_field($country);
1089 if (is_numeric($rate)) {
1090 $float_rate = (float) $rate;
1091 if ($float_rate >= 0 && $float_rate <= 100) {
1092 $sanitized[$sanitized_country] = $float_rate;
1093 }
1094 }
1095 }
1096 return $sanitized;
1097 }
1098
1099 /**
1100 * Sanitize booking form configuration
1101 */
1102 private function sanitize_booking_form_config(array $config): array
1103 {
1104 $sanitized = [];
1105 $allowed_form_types = ['contact_form', 'emergency_contact_form', 'traveler_form'];
1106 $allowed_field_types = ['text', 'email', 'tel', 'date', 'select', 'country', 'textarea', 'checkbox', 'number', 'text_block'];
1107 $allowed_widths = ['full', 'half', 'third'];
1108
1109 foreach ($config as $form_type => $form_config) {
1110 if (!in_array($form_type, $allowed_form_types, true)) {
1111 continue;
1112 }
1113
1114 $sanitized[$form_type] = [
1115 'title' => isset($form_config['title']) ? sanitize_text_field($form_config['title']) : '',
1116 'description' => isset($form_config['description']) ? sanitize_text_field($form_config['description']) : '',
1117 'enabled' => isset($form_config['enabled']) ? (bool) $form_config['enabled'] : true,
1118 'fields' => [],
1119 ];
1120
1121 if (!empty($form_config['fields']) && is_array($form_config['fields'])) {
1122 foreach ($form_config['fields'] as $field) {
1123 if (!is_array($field) || empty($field['id'])) {
1124 continue;
1125 }
1126
1127 $sanitized_field = [
1128 'id' => sanitize_key($field['id']),
1129 'type' => in_array($field['type'] ?? 'text', $allowed_field_types, true) ? $field['type'] : 'text',
1130 'label' => isset($field['label']) ? sanitize_text_field($field['label']) : '',
1131 'placeholder' => isset($field['placeholder']) ? sanitize_text_field($field['placeholder']) : '',
1132 'required' => isset($field['required']) ? (bool) $field['required'] : false,
1133 'enabled' => isset($field['enabled']) ? (bool) $field['enabled'] : true,
1134 'order' => isset($field['order']) ? (int) $field['order'] : 0,
1135 'width' => in_array($field['width'] ?? 'full', $allowed_widths, true) ? ($field['width'] ?? 'full') : 'full',
1136 'locked' => isset($field['locked']) ? (bool) $field['locked'] : false,
1137 ];
1138
1139 // Handle optional section
1140 if (!empty($field['section'])) {
1141 $sanitized_field['section'] = sanitize_key($field['section']);
1142 }
1143
1144 // Per-traveler targeting — Traveler section only. Whitelist
1145 // the allowed values; only persist the non-default "lead" so
1146 // other sections and existing configs stay byte-identical.
1147 if (
1148 $form_type === 'traveler_form'
1149 && ($field['applies_to'] ?? 'all') === 'lead'
1150 ) {
1151 $sanitized_field['applies_to'] = 'lead';
1152 }
1153
1154 // Handle options for select fields
1155 if ($sanitized_field['type'] === 'select' && !empty($field['options']) && is_array($field['options'])) {
1156 $sanitized_field['options'] = [];
1157 foreach ($field['options'] as $option) {
1158 if (is_array($option) && isset($option['value'])) {
1159 $sanitized_field['options'][] = [
1160 'value' => sanitize_key($option['value']),
1161 'label' => isset($option['label']) ? sanitize_text_field($option['label']) : $option['value'],
1162 ];
1163 }
1164 }
1165 }
1166
1167 // A text block is display-only content placed between fields:
1168 // keep its (safe-HTML) content, and it can never be required.
1169 if ($sanitized_field['type'] === 'text_block') {
1170 $sanitized_field['content'] = isset($field['content']) ? wp_kses_post($field['content']) : '';
1171 $sanitized_field['required'] = false;
1172 }
1173
1174 // Phone fields: the country-code selector is ON by default.
1175 // Only persist the non-default `false`, so existing configs
1176 // (which never carried this key) stay byte-identical and read
1177 // back as ON.
1178 if (
1179 $sanitized_field['type'] === 'tel'
1180 && array_key_exists('show_country_code', $field)
1181 && !$field['show_country_code']
1182 ) {
1183 $sanitized_field['show_country_code'] = false;
1184 }
1185
1186 $sanitized[$form_type]['fields'][] = $sanitized_field;
1187 }
1188
1189 // Sort fields by order
1190 usort($sanitized[$form_type]['fields'], function($a, $b) {
1191 return ($a['order'] ?? 0) - ($b['order'] ?? 0);
1192 });
1193 }
1194 }
1195
1196 return apply_filters('yatra_save_booking_form_config', $sanitized, $config);
1197 }
1198
1199 /**
1200 * Flush rewrite rules
1201 */
1202 public function flush_rewrite_rules(WP_REST_Request $request)
1203 {
1204 try {
1205 // Flush rewrite rules
1206 flush_rewrite_rules(true);
1207
1208 return $this->success_response([
1209 'message' => 'Rewrite rules flushed successfully',
1210 ]);
1211 } catch (\Exception $e) {
1212 return $this->error_response($e->getMessage(), 500);
1213 }
1214 }
1215
1216 /**
1217 * Get list of WordPress pages for booking page selection
1218 * Note: We don't check for shortcode here - it's checked on-demand when user selects a page
1219 */
1220 public function get_pages(WP_REST_Request $request)
1221 {
1222 try {
1223 $pages = get_pages([
1224 'post_status' => 'publish',
1225 'sort_column' => 'post_title',
1226 'sort_order' => 'ASC',
1227 ]);
1228
1229 $page_list = [];
1230 foreach ($pages as $page) {
1231 $page_list[] = [
1232 'id' => $page->ID,
1233 'title' => $page->post_title,
1234 'slug' => $page->post_name,
1235 'url' => get_permalink($page->ID),
1236 ];
1237 }
1238
1239 return $this->success_response($page_list);
1240 } catch (\Exception $e) {
1241 return $this->error_response($e->getMessage(), 500);
1242 }
1243 }
1244
1245 /**
1246 * Check if a page has the booking shortcode
1247 */
1248 public function check_booking_shortcode(WP_REST_Request $request)
1249 {
1250 try {
1251 $page_id = (int) $request->get_param('page_id');
1252
1253 if ($page_id <= 0) {
1254 return $this->error_response('Invalid page ID', 400);
1255 }
1256
1257 $page = get_post($page_id);
1258
1259 if (!$page || $page->post_type !== 'page') {
1260 return $this->error_response('Page not found', 404);
1261 }
1262
1263 $has_shortcode = has_shortcode($page->post_content, 'yatra_booking');
1264
1265 return $this->success_response([
1266 'page_id' => $page_id,
1267 'has_shortcode' => $has_shortcode,
1268 'page_title' => $page->post_title,
1269 'page_url' => get_permalink($page_id),
1270 'edit_url' => get_edit_post_link($page_id, 'raw'),
1271 ]);
1272 } catch (\Exception $e) {
1273 return $this->error_response($e->getMessage(), 500);
1274 }
1275 }
1276
1277 /**
1278 * Insert booking shortcode into a page
1279 */
1280 public function insert_booking_shortcode(WP_REST_Request $request)
1281 {
1282 try {
1283 $page_id = (int) $request->get_param('page_id');
1284
1285 if ($page_id <= 0) {
1286 return $this->error_response('Invalid page ID', 400);
1287 }
1288
1289 $page = get_post($page_id);
1290
1291 if (!$page || $page->post_type !== 'page') {
1292 return $this->error_response('Page not found', 404);
1293 }
1294
1295 // Check if shortcode already exists
1296 if (has_shortcode($page->post_content, 'yatra_booking')) {
1297 return $this->success_response([
1298 'message' => 'Shortcode already exists on this page',
1299 'page_id' => $page_id,
1300 'already_exists' => true,
1301 ]);
1302 }
1303
1304 // Append shortcode to page content
1305 $new_content = $page->post_content . "\n\n[yatra_booking]";
1306
1307 $result = wp_update_post([
1308 'ID' => $page_id,
1309 'post_content' => $new_content,
1310 ], true);
1311
1312 if (is_wp_error($result)) {
1313 return $this->error_response($result->get_error_message(), 500);
1314 }
1315
1316 return $this->success_response([
1317 'message' => 'Shortcode added successfully',
1318 'page_id' => $page_id,
1319 'page_url' => get_permalink($page_id),
1320 ]);
1321 } catch (\Exception $e) {
1322 return $this->error_response($e->getMessage(), 500);
1323 }
1324 }
1325
1326 /**
1327 * Keep Settings → Customer "account page" path aligned with {@see RouteMatcher} / {@see Router} (yatra_account_base).
1328 *
1329 * @param array<string, mixed> $settings
1330 * @return array<string, mixed>
1331 */
1332 private function syncAccountRouteSettingsForResponse(array $settings): array
1333 {
1334 // Prefer the full saved path so admin "View" matches Settings → Customer (not only yatra_account_base slug).
1335 $savedPath = get_option('yatra_customer_account_page', '');
1336 if (is_string($savedPath) && $savedPath !== '' && $savedPath !== '0') {
1337 $normalized = '/' . trim(str_replace('\\', '/', $savedPath), '/');
1338 if ($normalized === '/') {
1339 $normalized = '/my-account';
1340 }
1341 $settings['customer_account_page'] = $normalized;
1342
1343 return $settings;
1344 }
1345
1346 $stored = get_option('yatra_account_base', '');
1347 if (is_string($stored) && $stored !== '') {
1348 $settings['customer_account_page'] = '/' . $stored;
1349
1350 return $settings;
1351 }
1352
1353 $cpp = (string) ($settings['customer_account_page'] ?? '');
1354 $slug = self::accountSlugFromCustomerAccountPath($cpp !== '' ? $cpp : '/account');
1355 update_option('yatra_account_base', $slug);
1356 $settings['customer_account_page'] = '/' . $slug;
1357
1358 return $settings;
1359 }
1360
1361 private function persistAccountBaseFromCustomerAccountPage(): void
1362 {
1363 $cpp = (string) get_option('yatra_customer_account_page', '');
1364 update_option('yatra_account_base', self::accountSlugFromCustomerAccountPath($cpp));
1365 }
1366
1367 private static function accountSlugFromCustomerAccountPath(string $path): string
1368 {
1369 $path = trim(str_replace('\\', '/', $path), '/');
1370 $parts = array_values(array_filter(explode('/', $path), static fn ($p) => $p !== ''));
1371 $segment = $parts !== [] ? end($parts) : 'account';
1372 $slug = sanitize_title($segment);
1373
1374 return $slug !== '' ? $slug : 'account';
1375 }
1376 }
1377
1378