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

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