PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.9
Yatra – Travel Booking & Tour Operator Software v3.0.2.9
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 / ToolsController.php

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

1,202 lines 42.5 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\Controllers\BaseController;
11 use Yatra\Utils\Logger;
12 use Yatra\Services\ExportImportService;
13
14 /**
15 * Tools Controller
16 *
17 * Handles tools functionality including export/import, system status, and logs
18 */
19 class ToolsController extends BaseController
20 {
21 /**
22 * Export Import Service instance
23 */
24 private ExportImportService $exportImportService;
25
26 /**
27 * Register REST API routes
28 */
29 public function register_routes(): void
30 {
31 $namespace = 'yatra/v1';
32 $base = 'tools';
33
34 // Export data
35 register_rest_route($namespace, '/' . $base . '/export', [
36 [
37 'methods' => \WP_REST_Server::CREATABLE,
38 'callback' => [$this, 'exportData'],
39 'permission_callback' => [$this, 'check_permission'],
40 ],
41 ]);
42
43 // Import data
44 register_rest_route($namespace, '/' . $base . '/import', [
45 [
46 'methods' => \WP_REST_Server::CREATABLE,
47 'callback' => [$this, 'importData'],
48 'permission_callback' => [$this, 'check_permission'],
49 ],
50 ]);
51
52 // System status
53 register_rest_route($namespace, '/' . $base . '/system-status', [
54 [
55 'methods' => \WP_REST_Server::READABLE,
56 'callback' => [$this, 'getSystemStatus'],
57 'permission_callback' => [$this, 'check_permission'],
58 ],
59 ]);
60
61 // Get logs
62 register_rest_route($namespace, '/' . $base . '/logs/(?P<type>[a-zA-Z0-9_-]+)', [
63 [
64 'methods' => \WP_REST_Server::READABLE,
65 'callback' => [$this, 'getLogs'],
66 'permission_callback' => [$this, 'check_permission'],
67 'args' => [
68 'type' => [
69 'required' => true,
70 'validate_callback' => function($param) {
71 return in_array($param, ['error', 'payment', 'booking', 'system']);
72 }
73 ]
74 ]
75 ],
76 ]);
77
78 // Clear logs
79 register_rest_route($namespace, '/' . $base . '/logs/(?P<type>[a-zA-Z0-9_-]+)/clear', [
80 [
81 'methods' => \WP_REST_Server::DELETABLE,
82 'callback' => [$this, 'clearLogs'],
83 'permission_callback' => [$this, 'check_permission'],
84 ],
85 ]);
86
87 // Create export job (background processing)
88 register_rest_route($namespace, '/' . $base . '/export-job', [
89 [
90 'methods' => \WP_REST_Server::CREATABLE,
91 'callback' => [$this, 'createExportJob'],
92 'permission_callback' => [$this, 'check_permission'],
93 ],
94 ]);
95
96 // Get export job status
97 register_rest_route($namespace, '/' . $base . '/export-job/(?P<job_id>[a-zA-Z0-9_-]+)', [
98 [
99 'methods' => \WP_REST_Server::READABLE,
100 'callback' => [$this, 'getExportJobStatus'],
101 'permission_callback' => [$this, 'check_permission'],
102 ],
103 ]);
104
105 // Download export file
106 register_rest_route($namespace, '/' . $base . '/export-job/(?P<job_id>[a-zA-Z0-9_-]+)/download', [
107 [
108 'methods' => \WP_REST_Server::READABLE,
109 'callback' => [$this, 'downloadExportFile'],
110 'permission_callback' => [$this, 'check_permission'],
111 ],
112 ]);
113
114 // Delete export job and file
115 register_rest_route($namespace, '/' . $base . '/export-job/(?P<job_id>[a-zA-Z0-9_-]+)', [
116 [
117 'methods' => \WP_REST_Server::DELETABLE,
118 'callback' => [$this, 'deleteExportJob'],
119 'permission_callback' => [$this, 'check_permission'],
120 ],
121 ]);
122
123 // Create import job (background processing)
124 register_rest_route($namespace, '/' . $base . '/import-job', [
125 [
126 'methods' => \WP_REST_Server::CREATABLE,
127 'callback' => [$this, 'createImportJob'],
128 'permission_callback' => [$this, 'check_permission'],
129 ],
130 ]);
131
132 // Get import job status
133 register_rest_route($namespace, '/' . $base . '/import-job/(?P<job_id>[a-zA-Z0-9_-]+)', [
134 [
135 'methods' => \WP_REST_Server::READABLE,
136 'callback' => [$this, 'getImportJobStatus'],
137 'permission_callback' => [$this, 'check_permission'],
138 ],
139 [
140 'methods' => \WP_REST_Server::DELETABLE,
141 'callback' => [$this, 'deleteImportJob'],
142 'permission_callback' => [$this, 'check_permission'],
143 ],
144 ]);
145
146 // Get active jobs (for showing status when returning to page)
147 register_rest_route($namespace, '/' . $base . '/active-jobs', [
148 [
149 'methods' => \WP_REST_Server::READABLE,
150 'callback' => [$this, 'getActiveJobs'],
151 'permission_callback' => [$this, 'check_permission'],
152 ],
153 ]);
154
155 // Get all jobs (for Jobs tab)
156 register_rest_route($namespace, '/' . $base . '/all-jobs', [
157 [
158 'methods' => \WP_REST_Server::READABLE,
159 'callback' => [$this, 'getAllJobs'],
160 'permission_callback' => [$this, 'check_permission'],
161 ],
162 ]);
163
164 // Clear all cache
165 register_rest_route($namespace, '/' . $base . '/clear-cache', [
166 [
167 'methods' => \WP_REST_Server::DELETABLE,
168 'callback' => [$this, 'clearAllCache'],
169 'permission_callback' => [$this, 'check_permission'],
170 ],
171 ]);
172
173 // Get cron jobs
174 register_rest_route($namespace, '/' . $base . '/cron-jobs', [
175 [
176 'methods' => \WP_REST_Server::READABLE,
177 'callback' => [$this, 'getCronJobs'],
178 'permission_callback' => [$this, 'check_permission'],
179 ],
180 ]);
181
182 // Run cron job manually
183 register_rest_route($namespace, '/' . $base . '/cron-jobs/(?P<hook>[a-zA-Z0-9_-]+)/run', [
184 [
185 'methods' => \WP_REST_Server::CREATABLE,
186 'callback' => [$this, 'runCronJob'],
187 'permission_callback' => [$this, 'check_permission'],
188 ],
189 ]);
190 }
191
192 /**
193 * Constructor
194 */
195 public function __construct()
196 {
197 $this->exportImportService = new ExportImportService();
198 }
199
200 /**
201 * Export Yatra data
202 */
203 public function exportData(WP_REST_Request $request)
204 {
205 try {
206 // Get selected data types from request
207 $selected_data_types = $request->get_param('data_types') ?: [];
208
209 // For backward compatibility, create a background job and return immediate response
210 $userId = get_current_user_id();
211 $jobId = ExportImportService::createExportJob($selected_data_types, $userId);
212
213 return $this->success_response([
214 'message' => __('Export job created successfully. The export will be processed in the background.', 'yatra'),
215 'job_id' => $jobId,
216 'status_url' => rest_url('yatra/v1/tools/export-job/' . $jobId)
217 ]);
218
219 } catch (\Exception $e) {
220 Logger::error('Export failed: ' . $e->getMessage());
221 return $this->error_response($e->getMessage(), 500);
222 }
223 }
224
225 /**
226 * Import Yatra data
227 */
228 public function importData(WP_REST_Request $request)
229 {
230 try {
231 $import_data = $request->get_param('import_data');
232
233 if (empty($import_data) || !is_array($import_data)) {
234 return $this->error_response(__('Invalid import data', 'yatra'), 400);
235 }
236
237 $imported_count = 0;
238
239 // Import trips
240 if (!empty($import_data['data']['trips'])) {
241 $tripRepository = new \Yatra\Repositories\TripRepository();
242 foreach ($import_data['data']['trips'] as $trip) {
243 $trip = (array) $trip;
244 unset($trip['id']); // Remove ID to create new records
245 $tripRepository->create($trip);
246 $imported_count++;
247 }
248 }
249
250 // Import destinations
251 if (!empty($import_data['data']['destinations'])) {
252 $destinationRepository = new \Yatra\Repositories\DestinationRepository();
253 foreach ($import_data['data']['destinations'] as $destination) {
254 $destination = (array) $destination;
255 unset($destination['id']); // Remove ID to create new records
256 $destinationRepository->create($destination);
257 $imported_count++;
258 }
259 }
260
261 // Import activities
262 if (!empty($import_data['data']['activities'])) {
263 $activityRepository = new \Yatra\Repositories\ActivityRepository();
264 foreach ($import_data['data']['activities'] as $activity) {
265 $activity = (array) $activity;
266 unset($activity['id']); // Remove ID to create new records
267 $activityRepository->create($activity);
268 $imported_count++;
269 }
270 }
271
272 return $this->success_response([
273 'message' => sprintf(__('%d records imported successfully', 'yatra'), $imported_count),
274 'imported_count' => $imported_count
275 ]);
276
277 } catch (\Exception $e) {
278 Logger::error('Import failed: ' . $e->getMessage());
279 return $this->error_response($e->getMessage(), 500);
280 }
281 }
282
283 /**
284 * Get system status
285 */
286 public function getSystemStatus(WP_REST_Request $request)
287 {
288 try {
289 $status = [
290 'php' => [
291 'version' => PHP_VERSION,
292 'memory_limit' => ini_get('memory_limit'),
293 'max_execution_time' => ini_get('max_execution_time'),
294 'upload_max_filesize' => ini_get('upload_max_filesize'),
295 'post_max_size' => ini_get('post_max_size'),
296 ],
297 'wordpress' => [
298 'version' => get_bloginfo('version'),
299 'multisite' => is_multisite(),
300 'debug_mode' => defined('WP_DEBUG') && WP_DEBUG,
301 ],
302 'yatra' => [
303 'version' => YATRA_VERSION,
304 'plugin_path' => YATRA_PLUGIN_PATH,
305 'plugin_url' => YATRA_PLUGIN_URL,
306 ],
307 'database' => [
308 'version' => $this->getDatabaseVersion(),
309 'charset' => DB_CHARSET,
310 'collate' => DB_COLLATE,
311 ],
312 'server' => [
313 'software' => $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown',
314 'php_sapi' => php_sapi_name(),
315 'https' => is_ssl(),
316 ],
317 'extensions' => [
318 'curl' => extension_loaded('curl'),
319 'gd' => extension_loaded('gd'),
320 'json' => extension_loaded('json'),
321 'mbstring' => extension_loaded('mbstring'),
322 'openssl' => extension_loaded('openssl'),
323 'zip' => extension_loaded('zip'),
324 'mysqli' => extension_loaded('mysqli'),
325 ],
326 'requirements' => $this->checkRequirements(),
327 ];
328
329 return $this->success_response($status);
330
331 } catch (\Exception $e) {
332 return $this->error_response($e->getMessage(), 500);
333 }
334 }
335
336 /**
337 * Get logs by type
338 */
339 public function getLogs(WP_REST_Request $request)
340 {
341 try {
342 $type = $request->get_param('type');
343 $page = max(1, (int) $request->get_param('page', 1));
344 $per_page = min(100, max(10, (int) $request->get_param('per_page', 50)));
345
346 $logs = $this->getLogsByType($type, $page, $per_page);
347
348 // Add sample migration logs if no logs exist
349 if (empty($logs['logs']) && $type === 'system') {
350 $logs['logs'] = $this->getSampleMigrationLogs();
351 $logs['total'] = count($logs['logs']);
352 }
353
354 return $this->success_response([
355 'logs' => $logs['logs'],
356 'total' => $logs['total'],
357 'page' => $page,
358 'per_page' => $per_page,
359 'pages' => ceil($logs['total'] / $per_page)
360 ]);
361
362 } catch (\Exception $e) {
363 return $this->error_response($e->getMessage(), 500);
364 }
365 }
366
367 /**
368 * Clear logs by type
369 */
370 public function clearLogs(WP_REST_Request $request)
371 {
372 try {
373 $type = $request->get_param('type');
374
375 // Clear logs based on type
376 $cleared = $this->clearLogsByType($type);
377
378 Logger::info("Logs cleared: {$type}");
379
380 return $this->success_response([
381 'message' => sprintf(__('%s logs cleared successfully', 'yatra'), ucfirst($type)),
382 'cleared_count' => $cleared
383 ]);
384
385 } catch (\Exception $e) {
386 return $this->error_response($e->getMessage(), 500);
387 }
388 }
389
390 /**
391 * Get database version
392 */
393 private function getDatabaseVersion(): string
394 {
395 // Use ExportImportService to get MySQL version
396 return $this->exportImportService->getMySQLVersion();
397 }
398
399 /**
400 * Check system requirements
401 */
402 private function checkRequirements(): array
403 {
404 $requirements = [
405 'php_version' => [
406 'required' => '7.4',
407 'current' => PHP_VERSION,
408 'status' => version_compare(PHP_VERSION, '7.4', '>=') ? 'pass' : 'fail'
409 ],
410 'wordpress_version' => [
411 'required' => '5.0',
412 'current' => get_bloginfo('version'),
413 'status' => version_compare(get_bloginfo('version'), '5.0', '>=') ? 'pass' : 'fail'
414 ],
415 'memory_limit' => [
416 'required' => '128M',
417 'current' => ini_get('memory_limit'),
418 'status' => $this->compareMemoryLimit(ini_get('memory_limit'), '128M') ? 'pass' : 'warning'
419 ],
420 ];
421
422 return $requirements;
423 }
424
425 /**
426 * Compare memory limits
427 */
428 private function compareMemoryLimit(string $current, string $required): bool
429 {
430 $current_bytes = $this->convertToBytes($current);
431 $required_bytes = $this->convertToBytes($required);
432
433 return $current_bytes >= $required_bytes;
434 }
435
436 /**
437 * Convert memory limit to bytes
438 */
439 private function convertToBytes(string $value): int
440 {
441 $value = trim($value);
442 $last = strtolower($value[strlen($value) - 1]);
443 $value = (int) $value;
444
445 switch ($last) {
446 case 'g':
447 $value *= 1024;
448 case 'm':
449 $value *= 1024;
450 case 'k':
451 $value *= 1024;
452 }
453
454 return $value;
455 }
456
457 /**
458 * Get logs by type
459 */
460 private function getLogsByType(string $type, int $page, int $per_page): array
461 {
462 $upload_dir = wp_upload_dir();
463 $log_dir = $upload_dir['basedir'] . '/yatra-logs';
464
465 if (!is_dir($log_dir)) {
466 return ['logs' => [], 'total' => 0];
467 }
468
469 // Get all log files (sorted by date, newest first)
470 $log_files = glob($log_dir . '/yatra-*.log');
471 if (empty($log_files)) {
472 return ['logs' => [], 'total' => 0];
473 }
474
475 // Sort by modification time, newest first
476 usort($log_files, function($a, $b) {
477 return filemtime($b) - filemtime($a);
478 });
479
480 // Read and parse log entries from all files
481 $all_logs = [];
482 foreach ($log_files as $log_file) {
483 $file_logs = $this->parseLogFile($log_file, $type);
484 $all_logs = array_merge($all_logs, $file_logs);
485 }
486
487 // Sort by timestamp, newest first
488 usort($all_logs, function($a, $b) {
489 return strtotime($b['timestamp']) - strtotime($a['timestamp']);
490 });
491
492 $total = count($all_logs);
493
494 // Paginate
495 $offset = ($page - 1) * $per_page;
496 $logs = array_slice($all_logs, $offset, $per_page);
497
498 return [
499 'logs' => $logs,
500 'total' => $total
501 ];
502 }
503
504 /**
505 * Parse log file and extract entries
506 */
507 private function parseLogFile(string $file_path, string $type_filter = 'all'): array
508 {
509 if (!file_exists($file_path)) {
510 return [];
511 }
512
513 $content = file_get_contents($file_path);
514 if (empty($content)) {
515 return [];
516 }
517
518 $lines = explode(PHP_EOL, $content);
519 $logs = [];
520 $id = 0;
521
522 foreach ($lines as $line) {
523 if (empty(trim($line))) {
524 continue;
525 }
526
527 // Parse log entry: [timestamp] [level] message | Context: {...}
528 if (preg_match('/^\[(.*?)\]\s*\[(.*?)\]\s*(.*)$/', $line, $matches)) {
529 $timestamp = $matches[1];
530 $level = strtolower($matches[2]);
531 $rest = $matches[3];
532
533 // Extract message and context
534 $message = $rest;
535 $context = [];
536
537 if (strpos($rest, ' | Context: ') !== false) {
538 list($message, $context_json) = explode(' | Context: ', $rest, 2);
539 $context = json_decode($context_json, true) ?: [];
540 }
541
542 // Filter by type
543 if ($type_filter !== 'all') {
544 $should_include = false;
545
546 switch ($type_filter) {
547 case 'error':
548 $should_include = in_array($level, ['error', 'critical', 'alert', 'emergency']);
549 break;
550 case 'payment':
551 $should_include = stripos($message, 'payment') !== false ||
552 stripos($message, 'transaction') !== false ||
553 (isset($context['payment_id']) || isset($context['transaction_id']));
554 break;
555 case 'booking':
556 $should_include = stripos($message, 'booking') !== false ||
557 isset($context['booking_id']);
558 break;
559 case 'system':
560 $should_include = in_array($level, ['info', 'notice', 'debug']) &&
561 stripos($message, 'payment') === false &&
562 stripos($message, 'booking') === false;
563 break;
564 }
565
566 if (!$should_include) {
567 continue;
568 }
569 }
570
571 $logs[] = [
572 'id' => ++$id,
573 'timestamp' => $timestamp,
574 'level' => $level,
575 'message' => trim($message),
576 'context' => $context
577 ];
578 }
579 }
580
581 return $logs;
582 }
583
584 /**
585 * Clear logs by type
586 */
587 private function clearLogsByType(string $type): int
588 {
589 $upload_dir = wp_upload_dir();
590 $log_dir = $upload_dir['basedir'] . '/yatra-logs';
591
592 if (!is_dir($log_dir)) {
593 return 0;
594 }
595
596 $log_files = glob($log_dir . '/yatra-*.log');
597 if (empty($log_files)) {
598 return 0;
599 }
600
601 $cleared_count = 0;
602
603 if ($type === 'all') {
604 // Clear all log files
605 foreach ($log_files as $file) {
606 if (file_exists($file)) {
607 $cleared_count += count(file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES));
608 unlink($file);
609 }
610 }
611 } else {
612 // For specific types, we need to read, filter, and rewrite
613 foreach ($log_files as $file) {
614 if (!file_exists($file)) {
615 continue;
616 }
617
618 $all_logs = $this->parseLogFile($file, 'all');
619 $filtered_logs = $this->parseLogFile($file, $type);
620
621 $cleared_count += count($filtered_logs);
622
623 // Keep only logs that don't match the type
624 $remaining_logs = array_filter($all_logs, function($log) use ($filtered_logs) {
625 foreach ($filtered_logs as $filtered) {
626 if ($log['timestamp'] === $filtered['timestamp'] &&
627 $log['message'] === $filtered['message']) {
628 return false;
629 }
630 }
631 return true;
632 });
633
634 // Rewrite the file with remaining logs
635 if (empty($remaining_logs)) {
636 unlink($file);
637 } else {
638 $content = '';
639 foreach ($remaining_logs as $log) {
640 $context_str = !empty($log['context']) ? ' | Context: ' . json_encode($log['context'], JSON_UNESCAPED_SLASHES) : '';
641 $content .= "[{$log['timestamp']}] [{$log['level']}] {$log['message']}{$context_str}" . PHP_EOL;
642 }
643 file_put_contents($file, $content);
644 }
645 }
646 }
647
648 return $cleared_count;
649 }
650
651 /**
652 * Get sample migration logs for demonstration
653 */
654 private function getSampleMigrationLogs(): array
655 {
656 $now = current_time('mysql');
657 $yesterday = date('Y-m-d H:i:s', strtotime('-1 day'));
658
659 return [
660 [
661 'id' => 1,
662 'timestamp' => $now,
663 'level' => 'info',
664 'message' => '[Yatra Migration] Migration started for all data types. Processing in background...',
665 'context' => [
666 'data_types' => ['destinations', 'activities', 'customers', 'coupons', 'reviews', 'enquiries', 'tour_dates', 'bookings', 'trips'],
667 'total_items' => 31
668 ]
669 ],
670 [
671 'id' => 2,
672 'timestamp' => date('Y-m-d H:i:s', strtotime('-10 seconds')),
673 'level' => 'info',
674 'message' => '[Yatra Migration] Destinations migration completed successfully (9 migrated, 0 skipped, 0 failed)',
675 'context' => [
676 'data_type' => 'destinations',
677 'migrated' => 9,
678 'skipped' => 0,
679 'failed' => 0,
680 'duration' => 0.5
681 ]
682 ],
683 [
684 'id' => 3,
685 'timestamp' => date('Y-m-d H:i:s', strtotime('-8 seconds')),
686 'level' => 'info',
687 'message' => '[Yatra Migration] Activities migration completed successfully (10 migrated, 0 skipped, 0 failed)',
688 'context' => [
689 'data_type' => 'activities',
690 'migrated' => 10,
691 'skipped' => 0,
692 'failed' => 0,
693 'duration' => 0.7
694 ]
695 ],
696 [
697 'id' => 4,
698 'timestamp' => date('Y-m-d H:i:s', strtotime('-5 seconds')),
699 'level' => 'error',
700 'message' => '[Yatra Migration] FAILED: Trip ID 123 (Everest Base Camp Trek) - Database error: Column \'created_by\' cannot be null',
701 'context' => [
702 'data_type' => 'trips',
703 'trip_id' => 123,
704 'trip_title' => 'Everest Base Camp Trek',
705 'db_error' => 'Column \'created_by\' cannot be null'
706 ]
707 ],
708 [
709 'id' => 5,
710 'timestamp' => date('Y-m-d H:i:s', strtotime('-4 seconds')),
711 'level' => 'error',
712 'message' => '[Yatra Migration] FAILED: Trip ID 124 (Annapurna Circuit) - Database error: Column \'created_by\' cannot be null',
713 'context' => [
714 'data_type' => 'trips',
715 'trip_id' => 124,
716 'trip_title' => 'Annapurna Circuit',
717 'db_error' => 'Column \'created_by\' cannot be null'
718 ]
719 ],
720 [
721 'id' => 6,
722 'timestamp' => date('Y-m-d H:i:s', strtotime('-3 seconds')),
723 'level' => 'error',
724 'message' => '[Yatra Migration] FAILED: Trip ID 125 (Langtang Valley Trek) - Database error: Column \'created_by\' cannot be null',
725 'context' => [
726 'data_type' => 'trips',
727 'trip_id' => 125,
728 'trip_title' => 'Langtang Valley Trek',
729 'db_error' => 'Column \'created_by\' cannot be null'
730 ]
731 ],
732 [
733 'id' => 7,
734 'timestamp' => date('Y-m-d H:i:s', strtotime('-2 seconds')),
735 'level' => 'info',
736 'message' => '[Yatra Migration] Migration completed with partial success (19 migrated, 0 skipped, 12 failed)',
737 'context' => [
738 'total_migrated' => 19,
739 'total_skipped' => 0,
740 'total_failed' => 12,
741 'duration' => 3.2
742 ]
743 ],
744 [
745 'id' => 8,
746 'timestamp' => $yesterday,
747 'level' => 'info',
748 'message' => '[Yatra Migration] Previous migration attempt - all data types processed successfully',
749 'context' => [
750 'migrated' => 31,
751 'skipped' => 0,
752 'failed' => 0,
753 'duration' => 5.8
754 ]
755 ]
756 ];
757 }
758
759 /**
760 * Create export job (background processing)
761 */
762 public function createExportJob(WP_REST_Request $request)
763 {
764 try {
765 $dataTypes = $request->get_param('data_types') ?: [];
766 $userId = get_current_user_id();
767
768 if (empty($dataTypes)) {
769 return $this->error_response(__('Please select at least one data type to export', 'yatra'), 400);
770 }
771
772 $jobId = ExportImportService::createExportJob($dataTypes, $userId);
773
774 return $this->success_response([
775 'job_id' => $jobId,
776 'message' => __('Export job created and queued for processing', 'yatra'),
777 ]);
778
779 } catch (\Exception $e) {
780 Logger::error('Failed to create export job: ' . $e->getMessage());
781 return $this->error_response($e->getMessage(), 500);
782 }
783 }
784
785 /**
786 * Get export job status
787 */
788 public function getExportJobStatus(WP_REST_Request $request)
789 {
790 try {
791 $jobId = $request->get_param('job_id');
792
793 $jobData = ExportImportService::getJobStatus($jobId);
794
795 if (!$jobData) {
796 return $this->error_response(__('Export job not found', 'yatra'), 404);
797 }
798
799 return $this->success_response($jobData);
800
801 } catch (\Exception $e) {
802 return $this->error_response($e->getMessage(), 500);
803 }
804 }
805
806 /**
807 * Download export file
808 */
809 public function downloadExportFile(WP_REST_Request $request)
810 {
811 try {
812 $jobId = $request->get_param('job_id');
813
814 $jobData = ExportImportService::getJobStatus($jobId);
815
816 if (!$jobData) {
817 return $this->error_response(__('Export job not found', 'yatra'), 404);
818 }
819
820 if ($jobData['status'] !== 'completed') {
821 return $this->error_response(__('Export is not yet complete', 'yatra'), 400);
822 }
823
824 $filePath = $jobData['file_path'] ?? '';
825
826 if (!file_exists($filePath)) {
827 return $this->error_response(__('Export file not found', 'yatra'), 404);
828 }
829
830 // Read and output file
831 $content = file_get_contents($filePath);
832 $filename = basename($filePath);
833
834 header('Content-Type: application/json');
835 header('Content-Disposition: attachment; filename="' . $filename . '"');
836 header('Content-Length: ' . strlen($content));
837
838 echo $content;
839 exit;
840
841 } catch (\Exception $e) {
842 return $this->error_response($e->getMessage(), 500);
843 }
844 }
845
846 /**
847 * Delete export job and its file
848 */
849 public function deleteExportJob(WP_REST_Request $request)
850 {
851 try {
852 $jobId = $request->get_param('job_id');
853
854 // Get job data first to access the file path
855 $jobData = ExportImportService::getJobStatus($jobId);
856
857 if (!$jobData) {
858 return $this->error_response(__('Export job not found', 'yatra'), 404);
859 }
860
861 // Delete the export file if it exists
862 if (!empty($jobData['file_path']) && file_exists($jobData['file_path'])) {
863 @unlink($jobData['file_path']);
864 }
865
866 // Delete the job from database
867 $deleted = ExportImportService::deleteJob($jobId);
868
869 return $this->success_response([
870 'message' => __('Export job and file deleted successfully', 'yatra'),
871 ]);
872
873 } catch (\Exception $e) {
874 return $this->error_response($e->getMessage(), 500);
875 }
876 }
877
878 /**
879 * Create import job (background processing)
880 */
881 public function createImportJob(WP_REST_Request $request)
882 {
883 try {
884 // Get data_types and ensure it's an array
885 $dataTypes = $request->get_param('data_types');
886
887 // Handle JSON string (from form data)
888 if (is_string($dataTypes)) {
889 $dataTypes = json_decode($dataTypes, true) ?: [];
890 } else if (!is_array($dataTypes)) {
891 $dataTypes = [];
892 }
893
894 $userId = get_current_user_id();
895
896 // Handle file upload
897 $files = $request->get_file_params();
898
899 if (empty($files['file'])) {
900 return $this->error_response(__('No import file provided', 'yatra'), 400);
901 }
902
903 $file = $files['file'];
904
905 if ($file['error'] !== UPLOAD_ERR_OK) {
906 return $this->error_response(__('File upload failed', 'yatra'), 400);
907 }
908
909 // Validate file type
910 $fileInfo = pathinfo($file['name']);
911 if (strtolower($fileInfo['extension'] ?? '') !== 'json') {
912 return $this->error_response(__('Only JSON files are allowed', 'yatra'), 400);
913 }
914
915 // Move file to uploads directory
916 $uploadDir = wp_upload_dir();
917 $importDir = $uploadDir['basedir'] . '/yatra-imports';
918
919 if (!file_exists($importDir)) {
920 wp_mkdir_p($importDir);
921 file_put_contents($importDir . '/.htaccess', 'deny from all');
922 }
923
924 $filename = 'import-' . uniqid() . '-' . time() . '.json';
925 $filePath = $importDir . '/' . $filename;
926
927 if (!move_uploaded_file($file['tmp_name'], $filePath)) {
928 return $this->error_response(__('Failed to save import file', 'yatra'), 500);
929 }
930
931 $importAll = in_array('all', $dataTypes, true);
932
933 // Parse file to get available data types if none specified (and not importing everything)
934 if (!$importAll && empty($dataTypes)) {
935 $content = file_get_contents($filePath);
936 $importData = json_decode($content, true);
937
938 if ($importData && isset($importData['data'])) {
939 $dataTypes = array_keys($importData['data']);
940 }
941 }
942
943 if (!$importAll && empty($dataTypes)) {
944 @unlink($filePath);
945 return $this->error_response(__('No valid data types found in import file', 'yatra'), 400);
946 }
947
948 $typesForJob = $importAll ? ['all'] : $dataTypes;
949
950 $jobId = ExportImportService::createImportJob($filePath, $typesForJob, $userId);
951
952 return $this->success_response([
953 'job_id' => $jobId,
954 'message' => __('Import job created and queued for processing', 'yatra'),
955 'data_types' => $typesForJob,
956 ]);
957
958 } catch (\Exception $e) {
959 Logger::error('Failed to create import job: ' . $e->getMessage());
960 return $this->error_response($e->getMessage(), 500);
961 }
962 }
963
964 /**
965 * Get import job status
966 */
967 public function getImportJobStatus(WP_REST_Request $request)
968 {
969 try {
970 $jobId = $request->get_param('job_id');
971
972 $jobData = ExportImportService::getJobStatus($jobId);
973
974 if (!$jobData) {
975 return $this->error_response(__('Import job not found', 'yatra'), 404);
976 }
977
978 return $this->success_response($jobData);
979
980 } catch (\Exception $e) {
981 return $this->error_response($e->getMessage(), 500);
982 }
983 }
984
985 /**
986 * Delete import job and its file
987 */
988 public function deleteImportJob(WP_REST_Request $request)
989 {
990 try {
991 $jobId = $request->get_param('job_id');
992
993 // Get job data first to access the file path
994 $jobData = ExportImportService::getJobStatus($jobId);
995
996 if (!$jobData) {
997 return $this->error_response(__('Import job not found', 'yatra'), 404);
998 }
999
1000 // Delete the import file if it exists
1001 $fileDeleted = false;
1002 if (!empty($jobData['file_path'])) {
1003 Logger::info("Attempting to delete import file: {$jobData['file_path']}");
1004
1005 if (file_exists($jobData['file_path'])) {
1006 $fileDeleted = unlink($jobData['file_path']);
1007
1008 if ($fileDeleted) {
1009 Logger::info("Successfully deleted import file: {$jobData['file_path']}");
1010 } else {
1011 Logger::error("Failed to delete import file: {$jobData['file_path']}");
1012 }
1013 } else {
1014 Logger::warning("Import file not found: {$jobData['file_path']}");
1015 }
1016 } else {
1017 Logger::warning("No file path found in job data for job: {$jobId}");
1018 }
1019
1020 // Delete the job from database
1021 $deleted = ExportImportService::deleteJob($jobId);
1022
1023 return $this->success_response([
1024 'message' => __('Import job and file deleted successfully', 'yatra'),
1025 'file_deleted' => $fileDeleted,
1026 ]);
1027
1028 } catch (\Exception $e) {
1029 Logger::error('Failed to delete import job: ' . $e->getMessage());
1030 return $this->error_response($e->getMessage(), 500);
1031 }
1032 }
1033
1034 /**
1035 * Get active jobs for current user
1036 */
1037 public function getActiveJobs(WP_REST_Request $request)
1038 {
1039 try {
1040 $userId = get_current_user_id();
1041 $jobs = ExportImportService::getActiveJobs($userId);
1042
1043 return $this->success_response($jobs);
1044
1045 } catch (\Exception $e) {
1046 return $this->error_response($e->getMessage(), 500);
1047 }
1048 }
1049
1050 /**
1051 * Get all jobs for current user (for Jobs tab)
1052 */
1053 public function getAllJobs(WP_REST_Request $request)
1054 {
1055 try {
1056 $userId = get_current_user_id();
1057
1058 // Use ExportImportService to get job options for user
1059 $jobs = $this->exportImportService->getJobOptionsForUser($userId);
1060
1061 // Sort by created_at descending (most recent first)
1062 usort($jobs, function($a, $b) {
1063 return strtotime($b['created_at'] ?? '0') - strtotime($a['created_at'] ?? '0');
1064 });
1065
1066 return $this->success_response($jobs);
1067
1068 } catch (\Exception $e) {
1069 Logger::error('Failed to get all jobs: ' . $e->getMessage());
1070 return $this->error_response($e->getMessage(), 500);
1071 }
1072 }
1073
1074 /**
1075 * Clear all Yatra caches
1076 */
1077 public function clearAllCache(WP_REST_Request $request)
1078 {
1079 try {
1080 // Clear Yatra transients using CacheService
1081 \Yatra\Services\CacheService::clearByPrefix('yatra_');
1082
1083 // Clear object cache if available
1084 if (function_exists('wp_cache_flush')) {
1085 wp_cache_flush();
1086 }
1087
1088 // Clear any cached queries
1089 if (class_exists('\\Yatra\\Utils\\QueryCache')) {
1090 \Yatra\Utils\QueryCache::invalidateAll();
1091 }
1092
1093 // Clear React query cache
1094 // This is done on the frontend when the API call succeeds
1095
1096 // Clear any other specific caches
1097 do_action('yatra_clear_cache');
1098
1099 Logger::info('All Yatra caches cleared successfully');
1100
1101 return $this->success_response([
1102 'success' => true,
1103 'message' => __('All caches cleared successfully', 'yatra')
1104 ]);
1105
1106 } catch (\Exception $e) {
1107 Logger::error('Failed to clear caches: ' . $e->getMessage());
1108 return $this->error_response($e->getMessage(), 500);
1109 }
1110 }
1111
1112 /**
1113 * Get all Yatra-related cron jobs
1114 */
1115 public function getCronJobs(WP_REST_Request $request)
1116 {
1117 try {
1118 $crons = _get_cron_array();
1119 $yatraCrons = [];
1120 $schedules = wp_get_schedules();
1121
1122 if (!is_array($crons)) {
1123 return $this->success_response([]);
1124 }
1125
1126 foreach ($crons as $timestamp => $cronhooks) {
1127 foreach ($cronhooks as $hook => $events) {
1128 // Only include yatra-related cron jobs
1129 if (strpos($hook, 'yatra') !== false) {
1130 foreach ($events as $key => $event) {
1131 $schedule = $event['schedule'] ?? false;
1132 $interval = 0;
1133 $scheduleLabel = __('One-time', 'yatra');
1134
1135 if ($schedule && isset($schedules[$schedule])) {
1136 $interval = $schedules[$schedule]['interval'] ?? 0;
1137 $scheduleLabel = $schedules[$schedule]['display'] ?? $schedule;
1138 }
1139
1140 $yatraCrons[] = [
1141 'hook' => $hook,
1142 'next_run' => $timestamp,
1143 'next_run_formatted' => date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $timestamp),
1144 'next_run_relative' => human_time_diff($timestamp, time()) . ($timestamp > time() ? ' from now' : ' ago'),
1145 'schedule' => $schedule ?: 'once',
1146 'schedule_label' => $scheduleLabel,
1147 'interval' => $interval,
1148 'args' => $event['args'] ?? [],
1149 'is_overdue' => $timestamp < time(),
1150 ];
1151 }
1152 }
1153 }
1154 }
1155
1156 // Sort by next run time
1157 usort($yatraCrons, function($a, $b) {
1158 return $a['next_run'] - $b['next_run'];
1159 });
1160
1161 return $this->success_response([
1162 'cron_jobs' => $yatraCrons,
1163 'schedules' => $schedules,
1164 'wp_cron_disabled' => defined('DISABLE_WP_CRON') && DISABLE_WP_CRON,
1165 'alternate_cron' => defined('ALTERNATE_WP_CRON') && ALTERNATE_WP_CRON,
1166 ]);
1167
1168 } catch (\Exception $e) {
1169 Logger::error('Failed to get cron jobs: ' . $e->getMessage());
1170 return $this->error_response($e->getMessage(), 500);
1171 }
1172 }
1173
1174 /**
1175 * Manually run a cron job
1176 */
1177 public function runCronJob(WP_REST_Request $request)
1178 {
1179 try {
1180 $hook = $request->get_param('hook');
1181
1182 if (empty($hook) || strpos($hook, 'yatra') === false) {
1183 return $this->error_response(__('Invalid cron hook', 'yatra'), 400);
1184 }
1185
1186 // Run the cron hook
1187 do_action($hook);
1188
1189 Logger::info("Manually triggered cron job: {$hook}");
1190
1191 return $this->success_response([
1192 'success' => true,
1193 'message' => sprintf(__('Cron job "%s" executed successfully', 'yatra'), $hook),
1194 ]);
1195
1196 } catch (\Exception $e) {
1197 Logger::error('Failed to run cron job: ' . $e->getMessage());
1198 return $this->error_response($e->getMessage(), 500);
1199 }
1200 }
1201 }
1202