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

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

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