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

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