PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.6
Yatra – Travel Booking & Tour Operator Software v3.0.2.6
3.0.15 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 All 83 releases
yatra / app / Controllers / SettingsController.php

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

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