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

1,218 lines 49.3 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' => 'full_refund',
58 'cancellation_days' => 7,
59 'refund_policy' => '',
60 'booking_expiry_hours' => 24,
61 'booking_reminder_days' => 3,
62 'allow_waitlist' => true,
63 'waitlist_auto_confirm' => false,
64 // Pro: render available departure dates as a <select> instead of a
65 // flatpickr calendar on the single-trip sidebar (desktop + mobile).
66 'date_picker_as_dropdown' => false,
67
68 // Payment Settings
69 'currency' => 'USD',
70 'payment_test_mode' => true,
71 'payment_gateways' => [],
72 'payment_methods' => [],
73 'partial_payment' => false,
74 'partial_payment_percentage' => 30,
75 'deposit_required' => false,
76 'deposit_percentage' => 20,
77 'gateway_configs' => [],
78 'gateway_order' => [],
79
80 // Scheduled/Recurring Payment Settings (Pro feature - defaults disabled)
81 'enable_scheduled_payments' => false,
82 'scheduled_payment_type' => 'single', // single, installments
83 'scheduled_payment_days' => 15, // Days until first scheduled payment
84 'scheduled_payment_installments' => 1, // Number of installments (if type is installments)
85 'scheduled_payment_interval' => 30, // Days between installments
86 'scheduled_payment_reminder_days' => 3, // Days before to send reminder
87 'allow_save_payment_methods' => false,
88
89 // Email Settings (WordPress site defaults when Yatra options are missing)
90 'admin_email' => $wpAdminEmail,
91 'from_email' => $wpAdminEmail,
92 'from_name' => $wpSiteName,
93 'email_template_booking' => true,
94 'email_template_confirmation' => true,
95 'email_template_cancellation' => true,
96 'email_template_reminder' => true,
97 'email_template_admin_new_booking' => true,
98 'email_template_admin_payment' => true,
99 'email_template_admin_cancellation' => true,
100 'email_template_trip_consent' => true,
101 'email_template_customer_verification' => true,
102 'email_template_booking_completed' => true,
103 'email_template_booking_expired_customer' => true,
104 'email_template_admin_booking_expired' => true,
105 'email_template_scheduled_payment_reminder' => true,
106 'email_template_scheduled_payment_succeeded' => true,
107 'email_template_scheduled_payment_failed' => true,
108 'email_template_admin_scheduled_payment_failed' => true,
109 'email_template_enquiry_received' => true,
110 'email_template_enquiry_admin' => true,
111 'email_template_enquiry_response' => true,
112 'email_template_review_request' => true,
113 'email_template_abandoned_booking_recovery_first' => true,
114 'email_template_abandoned_booking_recovery_second' => true,
115 'email_template_abandoned_booking_recovery_final' => true,
116 'smtp_enabled' => false,
117 'smtp_host' => 'smtp.gmail.com',
118 'smtp_port' => 587,
119 'smtp_username' => '',
120 'smtp_password' => '',
121 'smtp_encryption' => 'tls',
122
123 // Customer Settings
124 'customer_registration' => true,
125 'customer_fields' => [],
126 'require_email_verification' => false,
127 'customer_account_page' => '',
128 'allow_customer_reviews' => true,
129 'customer_dashboard_enabled' => true,
130
131 // Review Settings
132 'enable_reviews' => true,
133 'require_booking' => true,
134 'auto_approve_reviews' => false,
135 'review_moderation' => true,
136 'min_rating' => 1,
137 'allow_anonymous_reviews' => false,
138 'review_reminder_days' => 7,
139
140 // Tax Settings
141 'enable_tax' => false,
142 'tax_name' => __('Tax', 'yatra'),
143 'tax_rate' => 0,
144 'tax_inclusive' => false,
145 'vat_number' => '',
146 'tax_by_country' => false,
147 'tax_rates' => [],
148 'multiple_taxes_enabled' => false,
149 'multiple_taxes' => [],
150 'multiple_taxes_by_country' => [],
151
152 // Currency Settings
153 'default_currency' => 'USD',
154 'multi_currency' => false,
155 'currency_position' => 'left',
156 'currency_decimals' => 2,
157 'decimal_separator'=>'.',
158 'thousand_separator'=>',',
159
160 // Notification Settings (SMS / future channels — booking email toggles live under Email → Templates)
161 'sms_notifications' => false,
162 'sms_provider' => '',
163 'sms_api_key' => '',
164
165 // Integration Settings
166 'google_analytics' => '',
167 'facebook_pixel' => '',
168 'recaptcha_enabled' => false,
169 'recaptcha_site_key' => '',
170 'recaptcha_secret_key' => '',
171
172 // Permalink Settings
173 'trip_base' => 'trip',
174 'destination_base' => 'destination',
175 'activity_base' => 'activity',
176 'trip_category_base' => 'trip-category',
177 'booking_base' => 'book',
178 // Wishlist (Pro) — stored in free options; active only when Pro + setting on
179 'enable_wishlist' => false,
180
181 // Booking Page Settings
182 'use_booking_page' => false,
183 'booking_page_id' => 0,
184
185 // Legal Pages (Booking UI)
186 'terms_page_id' => 0,
187 'privacy_policy_page_id' => 0,
188
189 // SEO Settings
190 'seo_trip_meta_title' => '',
191 'seo_trip_meta_description' => '',
192 'seo_trip_meta_keywords' => '',
193 'seo_trip_meta_image' => 0,
194
195 // Advanced Settings
196 'debug_mode' => false,
197 'enable_logging' => false,
198 'cache_enabled' => true,
199 'api_key' => '',
200 'api_rate_limit' => 100,
201 'session_timeout' => 3600,
202
203 // Booking Form Builder
204 'booking_form_config' => [],
205 ];
206
207 $base_settings = array_merge(
208 $base_settings,
209 \Yatra\Services\EmailTemplateDefaults::settingsOptionDefaults()
210 );
211
212 // Allow Pro plugins to add their settings via filter
213 $this->default_settings = apply_filters('yatra_settings_default_fields', $base_settings);
214 }
215
216 public function register_routes(): void
217 {
218 $namespace = 'yatra/v1';
219 $base = 'settings';
220
221 register_rest_route($namespace, '/' . $base, [
222 [
223 'methods' => \WP_REST_Server::READABLE,
224 'callback' => [$this, 'get_settings'],
225 'permission_callback' => [$this, 'check_permission'],
226 ],
227 [
228 'methods' => \WP_REST_Server::EDITABLE,
229 'callback' => [$this, 'update_settings'],
230 'permission_callback' => [$this, 'check_permission'],
231 ],
232 ]);
233
234 // Flush rewrite rules endpoint
235 register_rest_route($namespace, '/' . $base . '/flush-rewrite-rules', [
236 [
237 'methods' => \WP_REST_Server::CREATABLE,
238 'callback' => [$this, 'flush_rewrite_rules'],
239 'permission_callback' => [$this, 'check_permission'],
240 ],
241 ]);
242
243 // Get WordPress pages for booking page selection
244 register_rest_route($namespace, '/' . $base . '/pages', [
245 [
246 'methods' => \WP_REST_Server::READABLE,
247 'callback' => [$this, 'get_pages'],
248 'permission_callback' => [$this, 'check_permission'],
249 ],
250 ]);
251
252 // Check if page has booking shortcode
253 register_rest_route($namespace, '/' . $base . '/check-shortcode/(?P<page_id>\d+)', [
254 [
255 'methods' => \WP_REST_Server::READABLE,
256 'callback' => [$this, 'check_booking_shortcode'],
257 'permission_callback' => [$this, 'check_permission'],
258 ],
259 ]);
260
261 // Insert booking shortcode into page
262 register_rest_route($namespace, '/' . $base . '/insert-shortcode/(?P<page_id>\d+)', [
263 [
264 'methods' => \WP_REST_Server::CREATABLE,
265 'callback' => [$this, 'insert_booking_shortcode'],
266 'permission_callback' => [$this, 'check_permission'],
267 ],
268 ]);
269
270 register_rest_route($namespace, '/' . $base . '/email-template-preview', [
271 [
272 'methods' => \WP_REST_Server::CREATABLE,
273 'callback' => [$this, 'preview_core_email_template'],
274 'permission_callback' => [$this, 'check_permission'],
275 ],
276 ]);
277 }
278
279 /**
280 * Preview a core (settings-backed) transactional template with sample merge data.
281 */
282 public function preview_core_email_template(WP_REST_Request $request)
283 {
284 try {
285 $params = $request->get_json_params();
286 if (!is_array($params)) {
287 return $this->error_response(__('Invalid request body.', 'yatra'), 400);
288 }
289
290 $templateKey = sanitize_key($params['template_key'] ?? '');
291 $subjectTpl = sanitize_text_field($params['subject'] ?? '');
292 $bodyTpl = wp_kses_post($params['body'] ?? '');
293 $tripId = isset($params['trip_id']) ? (int) $params['trip_id'] : 0;
294 $tripId = $tripId > 0 ? $tripId : null;
295
296 $rendered = EmailTemplatePreviewService::render($templateKey, $subjectTpl, $bodyTpl, $tripId);
297
298 return $this->success_response([
299 'success' => true,
300 'data' => [
301 'subject' => $rendered['subject'],
302 'body' => $rendered['body'],
303 ],
304 ]);
305 } catch (\InvalidArgumentException $e) {
306 return $this->error_response($e->getMessage(), 400);
307 } catch (\Exception $e) {
308 return $this->error_response($e->getMessage(), 500);
309 }
310 }
311
312 public function check_permission(?WP_REST_Request $request = null): bool
313 {
314 if (!is_user_logged_in()) {
315 return false;
316 }
317
318 // Match other Yatra admin surfaces (e.g. Email Automation, Pro modules)
319 return current_user_can('manage_options')
320 || current_user_can('manage_yatra');
321 }
322
323 /**
324 * Get all settings
325 */
326 public function get_settings(WP_REST_Request $request)
327 {
328 try {
329 $settings = [];
330
331 // Get all settings from WordPress options table with yatra_ prefix
332 foreach ($this->default_settings as $key => $default_value) {
333 $option_name = 'yatra_' . $key;
334 $value = get_option($option_name, false);
335
336 // Only use default if option doesn't exist (wasn't set by InstallerService)
337 if ($value === false) {
338 $value = $default_value;
339 }
340
341 // Stored empty string should behave like "unset" for delivery identity (matches installer / backfill).
342 if (($key === 'admin_email' || $key === 'from_email') && is_string($value) && trim($value) === '') {
343 $wp = (string) get_option('admin_email', '');
344 $value = $wp !== '' ? $wp : $value;
345 }
346 if ($key === 'from_name' && is_string($value) && trim($value) === '') {
347 $wp = (string) get_bloginfo('name');
348 $value = $wp !== '' ? $wp : $value;
349 }
350
351 // Handle serialized arrays (for fields like payment_gateways, customer_fields, etc.)
352 if (is_string($value) && is_serialized($value)) {
353 $value = maybe_unserialize($value);
354 }
355
356 // Ensure arrays are returned as arrays (not objects)
357 if (is_array($default_value) && !is_array($value)) {
358 $value = [];
359 }
360
361 $settings[$key] = $value;
362 }
363
364 // Special handling for booking_form_config - always use getBookingFormConfig which handles locked fields
365 $settings['booking_form_config'] = \Yatra\Services\SettingsService::getBookingFormConfig();
366
367 // Merge in flexible payment settings from Pro module if enabled
368 $flexible_payment_settings = apply_filters('yatra_get_flexible_payment_settings', []);
369 if (!empty($flexible_payment_settings)) {
370 $settings = array_merge($settings, $flexible_payment_settings);
371 }
372
373 $scheduled_payment_settings = apply_filters('yatra_get_scheduled_payment_settings', []);
374 if (!empty($scheduled_payment_settings)) {
375 $settings = array_merge($settings, $scheduled_payment_settings);
376 }
377
378 // Scheduled payment keys are owned by Pro (yatra_pro_scheduled_payments), not yatra_* options.
379 foreach (
380 [
381 'enable_scheduled_payments',
382 'scheduled_payment_type',
383 'scheduled_payment_days',
384 'scheduled_payment_installments',
385 'scheduled_payment_interval',
386 'scheduled_payment_reminder_days',
387 ] as $sk
388 ) {
389 if (array_key_exists($sk, $this->default_settings)) {
390 $settings[$sk] = \Yatra\Services\SettingsService::get(
391 $sk,
392 $this->default_settings[$sk]
393 );
394 }
395 }
396
397 $settings = $this->syncAccountRouteSettingsForResponse($settings);
398
399 /**
400 * Allow Pro modules to align REST payloads with canonical option stores
401 * (e.g. GA4 settings that also live in yatra_google_analytics_settings).
402 */
403 $settings = apply_filters('yatra_rest_settings', $settings);
404
405 return $this->success_response($settings);
406 } catch (\Exception $e) {
407 return $this->error_response($e->getMessage(), 500);
408 }
409 }
410
411 /**
412 * Update settings
413 */
414 public function update_settings(WP_REST_Request $request)
415 {
416 try {
417 $data = $request->get_json_params();
418
419 if (!is_array($data)) {
420 return $this->error_response('Invalid settings data', 400);
421 }
422
423 $updated = [];
424 $errors = [];
425
426 // Check if Dynamic Form Field module is enabled
427 $is_dynamic_form_enabled = apply_filters('yatra_dynamic_form_field_enabled', false);
428
429 // Check if Flexible Payments module is enabled (Pro feature)
430 $is_flexible_payments_enabled = apply_filters('yatra_flexible_payments_enabled', false);
431
432 $is_scheduled_payments_module = apply_filters('yatra_scheduled_payments_module_active', false);
433
434 // Flexible payment settings keys (Pro only)
435 $flexible_payment_keys = [
436 'deposit_required', 'deposit_percentage', 'partial_payment',
437 'partial_payment_percentage', 'enable_deposit', 'allow_save_payment_methods',
438 ];
439
440 $scheduled_payment_keys = [
441 'enable_scheduled_payments',
442 'scheduled_payment_type',
443 'scheduled_payment_days',
444 'scheduled_payment_installments',
445 'scheduled_payment_interval',
446 'scheduled_payment_reminder_days',
447 ];
448
449 // Collect flexible payment settings to delegate to Pro
450 $flexible_payment_settings = [];
451
452 $scheduled_payment_settings_batch = [];
453
454 // Process each setting
455 foreach ($data as $key => $value) {
456 // Skip booking_form_config if Dynamic Form Field module is not enabled
457 // This allows the settings to save without error when the module is disabled
458 if ($key === 'booking_form_config' && !$is_dynamic_form_enabled) {
459 continue;
460 }
461
462 // Delegate flexible payment settings to Pro module
463 if (in_array($key, $flexible_payment_keys, true)) {
464 if ($is_flexible_payments_enabled) {
465 $flexible_payment_settings[$key] = $value;
466 }
467 // Skip saving in Free plugin - Pro handles these
468 continue;
469 }
470
471 if (in_array($key, $scheduled_payment_keys, true)) {
472 if ($is_scheduled_payments_module) {
473 $scheduled_payment_settings_batch[$key] = $value;
474 }
475 continue;
476 }
477
478 // Wishlist toggle: only meaningful with Yatra Pro active
479 if ($key === 'enable_wishlist' && !apply_filters('yatra_is_pro_active', false)) {
480 continue;
481 }
482
483 // Validate that the key exists in default settings
484 if (!array_key_exists($key, $this->default_settings)) {
485 $errors[] = sprintf('Unknown setting: %s', $key);
486 continue;
487 }
488
489 // Sanitize and validate the value based on its type
490 $sanitized_value = $this->sanitize_setting($key, $value);
491
492 if ($sanitized_value === null) {
493 $errors[] = sprintf('Invalid value for setting: %s', $key);
494 continue;
495 }
496
497 // Save to WordPress options table with yatra_ prefix
498 $option_name = 'yatra_' . $key;
499
500 // Serialize arrays for storage
501 if (is_array($sanitized_value)) {
502 $sanitized_value = maybe_serialize($sanitized_value);
503 }
504
505 $result = update_option($option_name, $sanitized_value);
506
507 if ($result !== false) {
508 $updated[] = $key;
509 }
510 }
511
512 // Delegate flexible payment settings to Pro module for saving
513 if (!empty($flexible_payment_settings) && $is_flexible_payments_enabled) {
514 do_action('yatra_save_flexible_payment_settings', $flexible_payment_settings);
515 $updated = array_merge($updated, array_keys($flexible_payment_settings));
516 }
517
518 if (!empty($scheduled_payment_settings_batch) && $is_scheduled_payments_module) {
519 do_action('yatra_save_scheduled_payment_settings', $scheduled_payment_settings_batch);
520 $updated = array_merge($updated, array_keys($scheduled_payment_settings_batch));
521 }
522
523 // Sync currency keys: keep 'currency' and 'default_currency' in sync
524 // Admin UI has both Payment Settings (currency) and Currency Settings (default_currency)
525 if (in_array('default_currency', $updated, true) && !in_array('currency', $updated, true)) {
526 $sync_currency = get_option('yatra_default_currency', 'USD');
527 update_option('yatra_currency', $sync_currency);
528 } elseif (in_array('currency', $updated, true) && !in_array('default_currency', $updated, true)) {
529 $sync_currency = get_option('yatra_currency', 'USD');
530 update_option('yatra_default_currency', $sync_currency);
531 }
532
533 if (in_array('customer_account_page', $updated, true)) {
534 $this->persistAccountBaseFromCustomerAccountPage();
535 }
536
537 if (!empty($errors)) {
538 $errorSummary = implode('; ', $errors);
539 return $this->error_response(
540 sprintf('Some settings could not be updated: %s', $errorSummary),
541 400,
542 [
543 'errors' => $errors,
544 'updated' => $updated,
545 ]
546 );
547 }
548
549 // Flush rewrite rules if permalink settings were updated
550 if (in_array('trip_base', $updated, true) ||
551 in_array('destination_base', $updated, true) ||
552 in_array('activity_base', $updated, true) ||
553 in_array('trip_category_base', $updated, true) ||
554 in_array('booking_base', $updated, true) ||
555 in_array('use_booking_page', $updated, true) ||
556 in_array('booking_page_id', $updated, true) ||
557 in_array('customer_account_page', $updated, true)) {
558 // Use hard flush to ensure rules are saved to database
559 flush_rewrite_rules(true);
560 }
561
562 if (!empty($updated)) {
563 \Yatra\Services\SettingsService::reload();
564 }
565
566 return $this->success_response([
567 'message' => 'Settings updated successfully',
568 'updated' => $updated,
569 ]);
570 } catch (\Exception $e) {
571 return $this->error_response($e->getMessage(), 500);
572 }
573 }
574
575 /**
576 * Sanitize and validate setting value
577 * Pro plugins can handle sanitization of their own settings via filter
578 *
579 * @param mixed $value
580 * @return mixed
581 */
582 private function sanitize_setting(string $key, $value)
583 {
584 $default = $this->default_settings[$key] ?? null;
585 $default_type = gettype($default);
586
587 // Allow Pro plugins to handle sanitization of their own settings
588 $filtered_value = apply_filters('yatra_sanitize_setting', null, $key, $value, $default);
589 if ($filtered_value !== null) {
590 return $filtered_value;
591 }
592
593 // Handle null values - use default
594 if ($value === null) {
595 return $default;
596 }
597
598 // Handle arrays
599 if (is_array($default)) {
600 if (!is_array($value)) {
601 return null;
602 }
603 // Sanitize array values
604 return array_map(function($item) {
605 if (is_string($item)) {
606 return sanitize_text_field($item);
607 }
608 if (is_numeric($item)) {
609 return is_float($item) ? (float) $item : (int) $item;
610 }
611 if (is_bool($item)) {
612 return (bool) $item;
613 }
614 if (is_array($item)) {
615 return $this->sanitize_array($item);
616 }
617 return $item;
618 }, $value);
619 }
620
621 // Handle booleans (REST may send true/false strings)
622 if (is_bool($default)) {
623 if (is_bool($value)) {
624 return $value;
625 }
626 if (is_string($value)) {
627 $parsed = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
628 return $parsed !== null ? $parsed : (bool) $value;
629 }
630 return (bool) $value;
631 }
632
633 // Handle integers
634 if (is_int($default)) {
635 if (!is_numeric($value)) {
636 return null;
637 }
638 $int_value = (int) $value;
639 // Validate ranges for specific fields
640 if ($key === 'cancellation_days' && $int_value < 0) {
641 return null;
642 }
643 if ($key === 'booking_expiry_hours' && $int_value < 0) {
644 return null;
645 }
646 if ($key === 'partial_payment_percentage' && ($int_value < 0 || $int_value > 100)) {
647 return null;
648 }
649 if ($key === 'deposit_percentage' && ($int_value < 0 || $int_value > 100)) {
650 return null;
651 }
652 if ($key === 'tax_rate' && ($int_value < 0 || $int_value > 100)) {
653 return null;
654 }
655 if ($key === 'smtp_port' && ($int_value < 1 || $int_value > 65535)) {
656 return null;
657 }
658 return $int_value;
659 }
660
661 // Handle floats
662 if (is_float($default)) {
663 if (!is_numeric($value)) {
664 return null;
665 }
666 $float_value = (float) $value;
667 if ($float_value < 0) {
668 return null;
669 }
670 return $float_value;
671 }
672
673 // Handle strings
674 if (is_string($default)) {
675 if ($key === 'timezone') {
676 $tz = is_string($value) ? trim($value) : '';
677 if ($tz === '') {
678 return is_string($default) ? $default : 'UTC';
679 }
680 try {
681 new \DateTimeZone($tz);
682
683 return $tz;
684 } catch (\Exception $e) {
685 return is_string($default) ? $default : 'UTC';
686 }
687 }
688 if ($key === 'currency_position') {
689 $allowed = ['left', 'right', 'left_space', 'right_space', 'before', 'after'];
690 $v = is_string($value) ? strtolower(trim($value)) : '';
691
692 return in_array($v, $allowed, true) ? $v : (is_string($default) ? $default : 'left');
693 }
694 // Special handling for specific fields
695 if ($key === 'company_email' || $key === 'admin_email' || $key === 'from_email' || $key === 'smtp_username') {
696 return sanitize_email($value);
697 }
698 if ($key === 'company_website' || $key === 'company_logo' || $key === 'google_analytics' || $key === 'facebook_pixel') {
699 return esc_url_raw($value);
700 }
701 if ($key === 'refund_policy' || $key === 'cancellation_policy') {
702 return sanitize_textarea_field($value);
703 }
704 if ($key === 'seo_trip_meta_title') {
705 // Allow more characters for meta title, but strip HTML
706 return wp_strip_all_tags($value);
707 }
708 if ($key === 'seo_trip_meta_description') {
709 // Allow more characters for meta description, but strip HTML
710 return wp_strip_all_tags($value);
711 }
712 if ($key === 'seo_trip_meta_keywords') {
713 // Allow keywords, strip HTML and sanitize
714 return sanitize_text_field($value);
715 }
716 if ($key === 'frontend_primary_color') {
717 return \Yatra\Utils\FrontendThemeCss::sanitizePrimaryColor(is_string($value) ? $value : '');
718 }
719 if ($key === 'frontend_container_max_width') {
720 return \Yatra\Utils\FrontendThemeCss::sanitizeContainerMaxWidthSetting(
721 is_string($value) ? $value : ''
722 );
723 }
724 if (is_string($key) && strpos($key, 'email_tpl_') === 0 && substr($key, -5) === '_body') {
725 return wp_kses_post((string) $value);
726 }
727 if (is_string($key) && strpos($key, 'email_tpl_') === 0 && substr($key, -8) === '_subject') {
728 return sanitize_text_field((string) $value);
729 }
730 if ($key === 'smtp_password' || $key === 'api_key' || $key === 'sms_api_key' || $key === 'recaptcha_secret_key') {
731 // Don't sanitize passwords/keys too aggressively
732 return sanitize_text_field($value);
733 }
734 if ($key === 'gateway_configs') {
735 // Handle nested array structure for gateway configs
736 if (is_array($value)) {
737 return $this->sanitize_gateway_configs($value);
738 }
739 return [];
740 }
741 if ($key === 'booking_form_config') {
742 // Handle nested array structure for booking form config
743 if (is_array($value)) {
744 return $this->sanitize_booking_form_config($value);
745 }
746 return [];
747 }
748 if ($key === 'tax_rates') {
749 // Handle nested array structure for tax rates
750 if (is_array($value)) {
751 return $this->sanitize_tax_rates($value);
752 }
753 return [];
754 }
755 return sanitize_text_field($value);
756 }
757
758 return $value;
759 }
760
761 /**
762 * Sanitize nested array
763 */
764 private function sanitize_array(array $array): array
765 {
766 $sanitized = [];
767 foreach ($array as $k => $v) {
768 $sanitized_key = is_string($k) ? sanitize_key($k) : $k;
769 if (is_array($v)) {
770 $sanitized[$sanitized_key] = $this->sanitize_array($v);
771 } elseif (is_string($v)) {
772 $sanitized[$sanitized_key] = sanitize_text_field($v);
773 } elseif (is_numeric($v)) {
774 $sanitized[$sanitized_key] = is_float($v) ? (float) $v : (int) $v;
775 } elseif (is_bool($v)) {
776 $sanitized[$sanitized_key] = (bool) $v;
777 } else {
778 $sanitized[$sanitized_key] = $v;
779 }
780 }
781 return $sanitized;
782 }
783
784 /**
785 * Sanitize gateway configs
786 */
787 private function sanitize_gateway_configs(array $configs): array
788 {
789 $sanitized = [];
790 foreach ($configs as $gateway => $config) {
791 if (!is_array($config)) {
792 continue;
793 }
794 $sanitized_gateway = sanitize_key($gateway);
795 $row = [
796 'enabled' => isset($config['enabled']) ? (bool) $config['enabled'] : false,
797 'icon' => isset($config['icon']) ? esc_url_raw($config['icon']) : '',
798 'title' => isset($config['title']) ? sanitize_text_field($config['title']) : '',
799 'description' => isset($config['description']) ? sanitize_textarea_field($config['description']) : '',
800 'api_key' => isset($config['api_key']) ? sanitize_text_field($config['api_key']) : '',
801 'api_secret' => isset($config['api_secret']) ? sanitize_text_field($config['api_secret']) : '',
802 'client_id' => isset($config['client_id']) ? sanitize_text_field($config['client_id']) : '',
803 'client_secret' => isset($config['client_secret']) ? sanitize_text_field($config['client_secret']) : '',
804 'merchant_id' => isset($config['merchant_id']) ? sanitize_text_field($config['merchant_id']) : '',
805 'public_key' => isset($config['public_key']) ? sanitize_text_field($config['public_key']) : '',
806 'private_key' => isset($config['private_key']) ? sanitize_text_field($config['private_key']) : '',
807 'webhook_secret' => isset($config['webhook_secret']) ? sanitize_text_field($config['webhook_secret']) : '',
808 'test_mode' => isset($config['test_mode']) ? (bool) $config['test_mode'] : false,
809 'sandbox' => isset($config['sandbox']) ? (bool) $config['sandbox'] : false,
810 ];
811
812 if ($sanitized_gateway === 'paypal') {
813 $mode = isset($config['mode']) && in_array((string) $config['mode'], ['simple', 'advanced'], true)
814 ? (string) $config['mode']
815 : 'simple';
816 $row['email'] = isset($config['email']) ? sanitize_email((string) $config['email']) : '';
817 $row['mode'] = $mode;
818 }
819
820 if ($sanitized_gateway === 'pay_later') {
821 $row['payment_deadline_days'] = isset($config['payment_deadline_days'])
822 ? max(1, min(60, (int) $config['payment_deadline_days']))
823 : 7;
824 $row['auto_cancel_days'] = isset($config['auto_cancel_days'])
825 ? max(0, min(30, (int) $config['auto_cancel_days']))
826 : 3;
827 $row['require_deposit'] = isset($config['require_deposit']) ? (bool) $config['require_deposit'] : false;
828 $row['deposit_amount'] = isset($config['deposit_amount'])
829 ? max(1, min(50, (int) $config['deposit_amount']))
830 : 10;
831 $row['reminder_days'] = isset($config['reminder_days'])
832 ? sanitize_text_field((string) $config['reminder_days'])
833 : '7,3,1';
834 }
835
836 if ($sanitized_gateway === 'stripe') {
837 $allowedStripeMethods = ['card', 'google_pay', 'apple_pay'];
838 $methodsRaw = isset($config['enabled_methods']) ? (string) $config['enabled_methods'] : '';
839 if ($methodsRaw !== '') {
840 $parts = array_filter(array_map('trim', explode(',', $methodsRaw)));
841 $normalized = [];
842 foreach ($parts as $part) {
843 $slug = strtolower($part);
844 if (in_array($slug, $allowedStripeMethods, true)) {
845 $normalized[] = $slug;
846 }
847 }
848 $row['enabled_methods'] = $normalized !== [] ? implode(',', $normalized) : 'card,google_pay,apple_pay';
849 } else {
850 $row['enabled_methods'] = 'card,google_pay,apple_pay';
851 }
852 foreach (['live_publishable_key', 'live_secret_key', 'test_publishable_key', 'test_secret_key'] as $stripeKey) {
853 if (array_key_exists($stripeKey, $config)) {
854 $row[$stripeKey] = sanitize_text_field((string) $config[$stripeKey]);
855 }
856 }
857 }
858
859 if ($sanitized_gateway === 'razorpay') {
860 $row['key_id'] = isset($config['key_id']) ? sanitize_text_field((string) $config['key_id']) : '';
861 $row['key_secret'] = isset($config['key_secret']) ? sanitize_text_field((string) $config['key_secret']) : '';
862 }
863
864 if ($sanitized_gateway === 'mollie') {
865 $row['api_key'] = isset($config['api_key']) ? sanitize_text_field((string) $config['api_key']) : '';
866 $row['webhook_url'] = isset($config['webhook_url']) ? esc_url_raw((string) $config['webhook_url']) : '';
867 $allowedMollie = ['creditcard', 'ideal', 'bancontact', 'sofort', 'eps', 'giropay', 'paypal', 'sepadirectdebit'];
868 $row['payment_methods'] = $this->sanitizeGatewayStringList(
869 $config['payment_methods'] ?? [],
870 $allowedMollie,
871 ['creditcard', 'ideal', 'paypal']
872 );
873 }
874
875 if ($sanitized_gateway === 'paystack') {
876 $row['public_key'] = isset($config['public_key']) ? sanitize_text_field((string) $config['public_key']) : '';
877 $row['secret_key'] = isset($config['secret_key']) ? sanitize_text_field((string) $config['secret_key']) : '';
878 $row['webhook_url'] = isset($config['webhook_url']) ? esc_url_raw((string) $config['webhook_url']) : '';
879 $allowedPaystack = ['card', 'bank', 'ussd', 'qr', 'mobile_money', 'bank_transfer'];
880 $row['payment_channels'] = $this->sanitizeGatewayStringList(
881 $config['payment_channels'] ?? [],
882 $allowedPaystack,
883 ['card', 'bank', 'ussd']
884 );
885 unset($row['private_key']);
886 }
887
888 if ($sanitized_gateway === 'square') {
889 $row['application_id'] = isset($config['application_id']) ? sanitize_text_field((string) $config['application_id']) : '';
890 $row['access_token'] = isset($config['access_token']) ? sanitize_text_field((string) $config['access_token']) : '';
891 $row['location_id'] = isset($config['location_id']) ? sanitize_text_field((string) $config['location_id']) : '';
892 }
893
894 if ($sanitized_gateway === 'authorize_net') {
895 $row['api_login_id'] = isset($config['api_login_id']) ? sanitize_text_field((string) $config['api_login_id']) : '';
896 $row['transaction_key'] = isset($config['transaction_key']) ? sanitize_text_field((string) $config['transaction_key']) : '';
897 $row['public_client_key'] = isset($config['public_client_key']) ? sanitize_text_field((string) $config['public_client_key']) : '';
898 }
899
900 if ($sanitized_gateway === 'bank_transfer') {
901 $row['bank_name'] = isset($config['bank_name']) ? sanitize_text_field((string) $config['bank_name']) : '';
902 $row['account_name'] = isset($config['account_name']) ? sanitize_text_field((string) $config['account_name']) : '';
903 $row['account_number'] = isset($config['account_number']) ? sanitize_text_field((string) $config['account_number']) : '';
904 $row['routing_code'] = isset($config['routing_code']) ? sanitize_text_field((string) $config['routing_code']) : '';
905 $row['instructions'] = isset($config['instructions']) ? sanitize_textarea_field((string) $config['instructions']) : '';
906 }
907
908 /**
909 * Allow Pro add-ons or custom code to append keys after core sanitization.
910 *
911 * @param array<string, mixed> $row
912 * @param array<string, mixed> $config
913 * @return array<string, mixed>
914 */
915 $row = apply_filters('yatra_sanitize_gateway_config_row', $row, $sanitized_gateway, $config);
916
917 $sanitized[$sanitized_gateway] = $row;
918 }
919 return $sanitized;
920 }
921
922 /**
923 * Normalize multiselect gateway options (Mollie methods, Paystack channels, etc.).
924 *
925 * @param mixed $input
926 * @param array<int, string> $allowed
927 * @param array<int, string> $default
928 * @return array<int, string>
929 */
930 private function sanitizeGatewayStringList($input, array $allowed, array $default): array
931 {
932 if (is_string($input) && $input !== '') {
933 $input = array_map('trim', explode(',', $input));
934 }
935 if (!is_array($input)) {
936 return $default;
937 }
938 $out = [];
939 foreach ($input as $v) {
940 $slug = sanitize_key((string) $v);
941 if ($slug !== '' && in_array($slug, $allowed, true)) {
942 $out[] = $slug;
943 }
944 }
945 $out = array_values(array_unique($out));
946
947 return $out !== [] ? $out : $default;
948 }
949
950 /**
951 * Sanitize tax rates
952 */
953 private function sanitize_tax_rates(array $rates): array
954 {
955 $sanitized = [];
956 foreach ($rates as $country => $rate) {
957 $sanitized_country = sanitize_text_field($country);
958 if (is_numeric($rate)) {
959 $float_rate = (float) $rate;
960 if ($float_rate >= 0 && $float_rate <= 100) {
961 $sanitized[$sanitized_country] = $float_rate;
962 }
963 }
964 }
965 return $sanitized;
966 }
967
968 /**
969 * Sanitize booking form configuration
970 */
971 private function sanitize_booking_form_config(array $config): array
972 {
973 $sanitized = [];
974 $allowed_form_types = ['contact_form', 'emergency_contact_form', 'traveler_form'];
975 $allowed_field_types = ['text', 'email', 'tel', 'date', 'select', 'country', 'textarea', 'checkbox', 'number'];
976 $allowed_widths = ['full', 'half', 'third'];
977
978 foreach ($config as $form_type => $form_config) {
979 if (!in_array($form_type, $allowed_form_types, true)) {
980 continue;
981 }
982
983 $sanitized[$form_type] = [
984 'title' => isset($form_config['title']) ? sanitize_text_field($form_config['title']) : '',
985 'description' => isset($form_config['description']) ? sanitize_text_field($form_config['description']) : '',
986 'enabled' => isset($form_config['enabled']) ? (bool) $form_config['enabled'] : true,
987 'fields' => [],
988 ];
989
990 if (!empty($form_config['fields']) && is_array($form_config['fields'])) {
991 foreach ($form_config['fields'] as $field) {
992 if (!is_array($field) || empty($field['id'])) {
993 continue;
994 }
995
996 $sanitized_field = [
997 'id' => sanitize_key($field['id']),
998 'type' => in_array($field['type'] ?? 'text', $allowed_field_types, true) ? $field['type'] : 'text',
999 'label' => isset($field['label']) ? sanitize_text_field($field['label']) : '',
1000 'placeholder' => isset($field['placeholder']) ? sanitize_text_field($field['placeholder']) : '',
1001 'required' => isset($field['required']) ? (bool) $field['required'] : false,
1002 'enabled' => isset($field['enabled']) ? (bool) $field['enabled'] : true,
1003 'order' => isset($field['order']) ? (int) $field['order'] : 0,
1004 'width' => in_array($field['width'] ?? 'full', $allowed_widths, true) ? $field['width'] : 'full',
1005 'locked' => isset($field['locked']) ? (bool) $field['locked'] : false,
1006 ];
1007
1008 // Handle optional section
1009 if (!empty($field['section'])) {
1010 $sanitized_field['section'] = sanitize_key($field['section']);
1011 }
1012
1013 // Handle options for select fields
1014 if ($sanitized_field['type'] === 'select' && !empty($field['options']) && is_array($field['options'])) {
1015 $sanitized_field['options'] = [];
1016 foreach ($field['options'] as $option) {
1017 if (is_array($option) && isset($option['value'])) {
1018 $sanitized_field['options'][] = [
1019 'value' => sanitize_key($option['value']),
1020 'label' => isset($option['label']) ? sanitize_text_field($option['label']) : $option['value'],
1021 ];
1022 }
1023 }
1024 }
1025
1026 $sanitized[$form_type]['fields'][] = $sanitized_field;
1027 }
1028
1029 // Sort fields by order
1030 usort($sanitized[$form_type]['fields'], function($a, $b) {
1031 return ($a['order'] ?? 0) - ($b['order'] ?? 0);
1032 });
1033 }
1034 }
1035
1036 return apply_filters('yatra_save_booking_form_config', $sanitized, $config);
1037 }
1038
1039 /**
1040 * Flush rewrite rules
1041 */
1042 public function flush_rewrite_rules(WP_REST_Request $request)
1043 {
1044 try {
1045 // Flush rewrite rules
1046 flush_rewrite_rules(true);
1047
1048 return $this->success_response([
1049 'message' => 'Rewrite rules flushed successfully',
1050 ]);
1051 } catch (\Exception $e) {
1052 return $this->error_response($e->getMessage(), 500);
1053 }
1054 }
1055
1056 /**
1057 * Get list of WordPress pages for booking page selection
1058 * Note: We don't check for shortcode here - it's checked on-demand when user selects a page
1059 */
1060 public function get_pages(WP_REST_Request $request)
1061 {
1062 try {
1063 $pages = get_pages([
1064 'post_status' => 'publish',
1065 'sort_column' => 'post_title',
1066 'sort_order' => 'ASC',
1067 ]);
1068
1069 $page_list = [];
1070 foreach ($pages as $page) {
1071 $page_list[] = [
1072 'id' => $page->ID,
1073 'title' => $page->post_title,
1074 'slug' => $page->post_name,
1075 'url' => get_permalink($page->ID),
1076 ];
1077 }
1078
1079 return $this->success_response($page_list);
1080 } catch (\Exception $e) {
1081 return $this->error_response($e->getMessage(), 500);
1082 }
1083 }
1084
1085 /**
1086 * Check if a page has the booking shortcode
1087 */
1088 public function check_booking_shortcode(WP_REST_Request $request)
1089 {
1090 try {
1091 $page_id = (int) $request->get_param('page_id');
1092
1093 if ($page_id <= 0) {
1094 return $this->error_response('Invalid page ID', 400);
1095 }
1096
1097 $page = get_post($page_id);
1098
1099 if (!$page || $page->post_type !== 'page') {
1100 return $this->error_response('Page not found', 404);
1101 }
1102
1103 $has_shortcode = has_shortcode($page->post_content, 'yatra_booking');
1104
1105 return $this->success_response([
1106 'page_id' => $page_id,
1107 'has_shortcode' => $has_shortcode,
1108 'page_title' => $page->post_title,
1109 'page_url' => get_permalink($page_id),
1110 'edit_url' => get_edit_post_link($page_id, 'raw'),
1111 ]);
1112 } catch (\Exception $e) {
1113 return $this->error_response($e->getMessage(), 500);
1114 }
1115 }
1116
1117 /**
1118 * Insert booking shortcode into a page
1119 */
1120 public function insert_booking_shortcode(WP_REST_Request $request)
1121 {
1122 try {
1123 $page_id = (int) $request->get_param('page_id');
1124
1125 if ($page_id <= 0) {
1126 return $this->error_response('Invalid page ID', 400);
1127 }
1128
1129 $page = get_post($page_id);
1130
1131 if (!$page || $page->post_type !== 'page') {
1132 return $this->error_response('Page not found', 404);
1133 }
1134
1135 // Check if shortcode already exists
1136 if (has_shortcode($page->post_content, 'yatra_booking')) {
1137 return $this->success_response([
1138 'message' => 'Shortcode already exists on this page',
1139 'page_id' => $page_id,
1140 'already_exists' => true,
1141 ]);
1142 }
1143
1144 // Append shortcode to page content
1145 $new_content = $page->post_content . "\n\n[yatra_booking]";
1146
1147 $result = wp_update_post([
1148 'ID' => $page_id,
1149 'post_content' => $new_content,
1150 ], true);
1151
1152 if (is_wp_error($result)) {
1153 return $this->error_response($result->get_error_message(), 500);
1154 }
1155
1156 return $this->success_response([
1157 'message' => 'Shortcode added successfully',
1158 'page_id' => $page_id,
1159 'page_url' => get_permalink($page_id),
1160 ]);
1161 } catch (\Exception $e) {
1162 return $this->error_response($e->getMessage(), 500);
1163 }
1164 }
1165
1166 /**
1167 * Keep Settings → Customer "account page" path aligned with {@see RouteMatcher} / {@see Router} (yatra_account_base).
1168 *
1169 * @param array<string, mixed> $settings
1170 * @return array<string, mixed>
1171 */
1172 private function syncAccountRouteSettingsForResponse(array $settings): array
1173 {
1174 // Prefer the full saved path so admin "View" matches Settings → Customer (not only yatra_account_base slug).
1175 $savedPath = get_option('yatra_customer_account_page', '');
1176 if (is_string($savedPath) && $savedPath !== '' && $savedPath !== '0') {
1177 $normalized = '/' . trim(str_replace('\\', '/', $savedPath), '/');
1178 if ($normalized === '/') {
1179 $normalized = '/my-account';
1180 }
1181 $settings['customer_account_page'] = $normalized;
1182
1183 return $settings;
1184 }
1185
1186 $stored = get_option('yatra_account_base', '');
1187 if (is_string($stored) && $stored !== '') {
1188 $settings['customer_account_page'] = '/' . $stored;
1189
1190 return $settings;
1191 }
1192
1193 $cpp = (string) ($settings['customer_account_page'] ?? '');
1194 $slug = self::accountSlugFromCustomerAccountPath($cpp !== '' ? $cpp : '/account');
1195 update_option('yatra_account_base', $slug);
1196 $settings['customer_account_page'] = '/' . $slug;
1197
1198 return $settings;
1199 }
1200
1201 private function persistAccountBaseFromCustomerAccountPage(): void
1202 {
1203 $cpp = (string) get_option('yatra_customer_account_page', '');
1204 update_option('yatra_account_base', self::accountSlugFromCustomerAccountPath($cpp));
1205 }
1206
1207 private static function accountSlugFromCustomerAccountPath(string $path): string
1208 {
1209 $path = trim(str_replace('\\', '/', $path), '/');
1210 $parts = array_values(array_filter(explode('/', $path), static fn ($p) => $p !== ''));
1211 $segment = $parts !== [] ? end($parts) : 'account';
1212 $slug = sanitize_title($segment);
1213
1214 return $slug !== '' ? $slug : 'account';
1215 }
1216 }
1217
1218