PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.4
Yatra – Travel Booking & Tour Operator Software v3.0.4
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 / Services / StatsUsage.php

StatsUsage.php in Yatra – Travel Booking & Tour Operator Software 3.0.4, at app/Services/StatsUsage.php

1,033 lines 37.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Yatra usage telemetry (opt-in, privacy-safe).
4 *
5 * @package Yatra\Services
6 */
7
8 declare(strict_types=1);
9
10 namespace Yatra\Services;
11
12 use Yatra\Core\Modules\ModuleManager;
13 use Yatra\Database\Tables\BookingsTable;
14 use Yatra\Database\Tables\ClassificationsTable;
15 use Yatra\Database\Tables\TripsTable;
16 use Yatra\Hooks\TelemetryHookNames;
17 use Yatra\Services\SettingsService;
18
19 defined('ABSPATH') || exit;
20
21 /**
22 * Centralized telemetry: consent, collection, cron, sync, local cache.
23 *
24 * Payload is intentionally minimal: environment + Yatra usage signals only.
25 * No customer PII, booking/traveler data, API keys, full plugin manifests,
26 * or hashed emails — only counts, booleans, and coarse compatibility hints.
27 *
28 * @since 3.0.0
29 */
30 final class StatsUsage
31 {
32 public const OPT_CONSENT = 'yatra_allow_usage_tracking';
33 public const OPT_INSTANCE_ID = 'yatra_usage_instance_id';
34 public const OPT_LAST_SYNC = 'yatra_usage_last_sync';
35 public const OPT_RETRY_COUNT = 'yatra_usage_retry_count';
36 public const OPT_NEXT_RETRY = 'yatra_usage_next_retry';
37 public const OPT_ONBOARDING_META = 'yatra_usage_onboarding_meta';
38 public const OPT_EVENT_COUNTERS = 'yatra_usage_event_counters';
39 public const OPT_LAST_PAYLOAD_HASH = 'yatra_usage_last_payload_hash';
40 /** @var string Last remote error for admin debugging (HTTP code, wp_remote body snippet). */
41 public const OPT_LAST_SEND_ERROR = 'yatra_usage_last_send_error';
42 public const TRANSIENT_SNAPSHOT = 'yatra_usage_snapshot_pending';
43 public const TRANSIENT_ADMIN_FALLBACK = 'yatra_usage_admin_fallback_throttle';
44 public const CRON_HOOK = 'yatra_usage_tracking_event';
45 public const IMMEDIATE_HOOK = 'yatra_usage_tracking_immediate';
46 /**
47 * Default ingest URL for the Usage hub (same route as pretty /wp-json/mantrabrain/v1/collect).
48 * Query-style rest_route is used so POSTs still reach WordPress when rewrites or edge proxies
49 * return a generic HTML 404 for /wp-json/... (common on some shared/CDN setups).
50 */
51 public const ENDPOINT = 'https://usage.mantrabrain.com/index.php?rest_route=/mantrabrain/v1/collect';
52
53 /**
54 * Remote collect URL. Override with wp-config constant YATRA_USAGE_TRACKING_ENDPOINT or filter `yatra_usage_tracking_endpoint`.
55 */
56 private static function get_remote_endpoint(): string
57 {
58 if (defined('YATRA_USAGE_TRACKING_ENDPOINT') && is_string(YATRA_USAGE_TRACKING_ENDPOINT) && YATRA_USAGE_TRACKING_ENDPOINT !== '') {
59 return (string) YATRA_USAGE_TRACKING_ENDPOINT;
60 }
61
62 return (string) apply_filters('yatra_usage_tracking_endpoint', self::ENDPOINT);
63 }
64
65 private static ?self $instance = null;
66
67 public static function instance(): self
68 {
69 if (self::$instance === null) {
70 self::$instance = new self();
71 }
72
73 return self::$instance;
74 }
75
76 public function init(): void
77 {
78 add_action(self::CRON_HOOK, [$this, 'cron_sync']);
79 add_action(self::IMMEDIATE_HOOK, [$this, 'cron_sync']);
80 add_action('admin_init', [$this, 'maybe_fallback_sync'], 30);
81 // Subscribed hook names must match do_action() emitters (see TelemetryHookNames).
82 add_action(TelemetryHookNames::BOOKING_CREATED, [$this, 'on_booking_created'], 20, 2);
83 add_action(TelemetryHookNames::TRIP_CREATED_WITH_RELATIONS, [$this, 'on_trip_created'], 20, 3);
84 add_action(TelemetryHookNames::SETUP_WIZARD_COMPLETED, [$this, 'on_wizard_completed']);
85 add_action(TelemetryHookNames::PAYMENT_GATEWAY_CONFIG_SAVED, [$this, 'on_gateway_config_saved'], 10, 2);
86 }
87
88 public function is_enabled(): bool
89 {
90 return (bool) get_option(self::OPT_CONSENT, false);
91 }
92
93 public function enable(bool $send_immediate = true): void
94 {
95 update_option(self::OPT_CONSENT, true);
96 $this->ensure_instance_id();
97 $this->schedule_weekly();
98 delete_option(self::OPT_RETRY_COUNT);
99 delete_option(self::OPT_NEXT_RETRY);
100 if ($send_immediate) {
101 wp_unschedule_hook(self::IMMEDIATE_HOOK);
102 wp_schedule_single_event(time() + 2, self::IMMEDIATE_HOOK);
103 if (!defined('DISABLE_WP_CRON') || !DISABLE_WP_CRON) {
104 spawn_cron();
105 }
106 }
107 }
108
109 public function disable(): void
110 {
111 update_option(self::OPT_CONSENT, false);
112 wp_clear_scheduled_hook(self::CRON_HOOK);
113 wp_clear_scheduled_hook(self::IMMEDIATE_HOOK);
114 delete_option(self::OPT_RETRY_COUNT);
115 delete_option(self::OPT_NEXT_RETRY);
116 delete_transient(self::TRANSIENT_SNAPSHOT);
117 delete_option(self::OPT_LAST_PAYLOAD_HASH);
118 }
119
120 public function schedule_weekly(): void
121 {
122 if (!wp_next_scheduled(self::CRON_HOOK)) {
123 wp_schedule_event(time() + HOUR_IN_SECONDS, 'weekly', self::CRON_HOOK);
124 }
125 }
126
127 /**
128 * Record a lightweight product event (local counter; included in next payload).
129 */
130 public function record_event(string $name, int $delta = 1): void
131 {
132 if (!$this->is_enabled()) {
133 return;
134 }
135 $name = sanitize_key($name);
136 if ($name === '' || $delta === 0) {
137 return;
138 }
139 $counters = get_option(self::OPT_EVENT_COUNTERS, []);
140 if (!is_array($counters)) {
141 $counters = [];
142 }
143 $counters[$name] = (int) ($counters[$name] ?? 0) + $delta;
144 update_option(self::OPT_EVENT_COUNTERS, $counters, false);
145 do_action('yatra_usage_tracking_event_recorded', $name, $delta, $counters);
146 }
147
148 public function cron_sync(): void
149 {
150 if (!$this->is_enabled()) {
151 return;
152 }
153 $next = (int) get_option(self::OPT_NEXT_RETRY, 0);
154 if ($next > time()) {
155 return;
156 }
157 $this->sync();
158 }
159
160 public function maybe_fallback_sync(): void
161 {
162 if (!is_admin() || !current_user_can('manage_options')) {
163 return;
164 }
165 if (!$this->is_enabled()) {
166 return;
167 }
168 if (get_transient(self::TRANSIENT_ADMIN_FALLBACK)) {
169 return;
170 }
171 $last = (int) get_option(self::OPT_LAST_SYNC, 0);
172 // If weekly cron likely missed (~9 days), try once while an admin is present.
173 if ($last > 0 && (time() - $last) < 9 * DAY_IN_SECONDS) {
174 return;
175 }
176 set_transient(self::TRANSIENT_ADMIN_FALLBACK, 1, 12 * HOUR_IN_SECONDS);
177 $this->sync();
178 }
179
180 public function on_booking_created(int $booking_id, $booking): void
181 {
182 unset($booking_id, $booking);
183 if (!$this->is_enabled()) {
184 return;
185 }
186 if (get_option('yatra_usage_flag_first_booking', '') === '1') {
187 return;
188 }
189 update_option('yatra_usage_flag_first_booking', '1', false);
190 $this->record_event('first_booking_received');
191 }
192
193 /**
194 * Trips are stored in {@see TripsTable}, not the legacy `tour` post type.
195 *
196 * @param array<string,mixed> $relationships
197 * @param array<string,mixed> $data
198 */
199 public function on_trip_created(int $trip_id, array $relationships, array $data): void
200 {
201 unset($trip_id, $relationships, $data);
202 if (!$this->is_enabled()) {
203 return;
204 }
205 if (get_option('yatra_usage_flag_first_trip', '') === '1') {
206 return;
207 }
208 update_option('yatra_usage_flag_first_trip', '1', false);
209 $this->record_event('first_trip_created');
210 }
211
212 public function on_wizard_completed(): void
213 {
214 $meta = $this->get_onboarding_meta();
215 $meta['completed_at'] = time();
216 $meta['onboarding_completed'] = true;
217 update_option(self::OPT_ONBOARDING_META, $meta, false);
218 if ($this->is_enabled()) {
219 $this->record_event('onboarding_completed');
220 }
221 }
222
223 /**
224 * @param array<string,mixed> $config
225 */
226 public function on_gateway_config_saved(string $gateway_id, array $config): void
227 {
228 unset($gateway_id, $config);
229 if (!$this->is_enabled()) {
230 return;
231 }
232 $this->record_event('payment_gateway_connected');
233 }
234
235 /**
236 * @return array<string,mixed>
237 */
238 public function get_onboarding_meta(): array
239 {
240 $m = get_option(self::OPT_ONBOARDING_META, []);
241 return is_array($m) ? $m : [];
242 }
243
244 /**
245 * @param array<string,mixed> $patch
246 */
247 public function patch_onboarding_meta(array $patch): void
248 {
249 $meta = array_merge($this->get_onboarding_meta(), $patch);
250 update_option(self::OPT_ONBOARDING_META, $meta, false);
251 }
252
253 public function mark_onboarding_started(): void
254 {
255 $meta = $this->get_onboarding_meta();
256 if (!empty($meta['started_at'])) {
257 return;
258 }
259 $meta['started_at'] = time();
260 $meta['onboarding_started'] = true;
261 update_option(self::OPT_ONBOARDING_META, $meta, false);
262 if ($this->is_enabled()) {
263 $this->record_event('onboarding_started');
264 }
265 }
266
267 public function set_onboarding_step(string $step): void
268 {
269 $this->patch_onboarding_meta([
270 'last_step' => sanitize_key($step),
271 ]);
272 }
273
274 /**
275 * Full sync: build payload, POST, handle retry/backoff.
276 *
277 * @param bool $require_opt_in When false, sends even if the site has not opted in (admin “test send” only).
278 */
279 public function sync(bool $require_opt_in = true): bool
280 {
281 if ($require_opt_in && !$this->is_enabled()) {
282 return false;
283 }
284 $this->ensure_instance_id();
285
286 $payload = $this->build_payload();
287 $json = wp_json_encode($payload);
288 if ($json === false) {
289 return false;
290 }
291
292 $hash = hash('sha256', $json);
293 $last_hash = (string) get_option(self::OPT_LAST_PAYLOAD_HASH, '');
294 // Avoid spamming identical payloads within the same day (cron + immediate + fallback).
295 if ($last_hash === $hash && (time() - (int) get_option(self::OPT_LAST_SYNC, 0)) < DAY_IN_SECONDS) {
296 return true;
297 }
298
299 set_transient(self::TRANSIENT_SNAPSHOT, $payload, HOUR_IN_SECONDS);
300
301 $endpoint = self::get_remote_endpoint();
302
303 $args = [
304 'timeout' => 20,
305 'headers' => [
306 'Content-Type' => 'application/json',
307 'X-Plugin' => 'yatra',
308 'X-Product' => 'yatra',
309 'X-Version' => defined('YATRA_VERSION') ? (string) YATRA_VERSION : '0',
310 ],
311 'body' => $json,
312 'blocking' => true,
313 ];
314
315 /**
316 * Filter wp_remote_post arguments for usage telemetry (e.g. add Authorization for a self-hosted receiver).
317 *
318 * @param array<string,mixed> $args Request arguments.
319 * @param array<string,mixed> $payload Encoded payload array (pre-JSON).
320 * @param string $endpoint Resolved remote URL after `yatra_usage_tracking_endpoint`.
321 */
322 $args = apply_filters('yatra_usage_tracking_remote_args', $args, $payload, $endpoint);
323
324 $response = wp_remote_post($endpoint, $args);
325
326 $code = wp_remote_retrieve_response_code($response);
327 $ok = !is_wp_error($response) && $code >= 200 && $code < 300;
328
329 if ($ok) {
330 update_option(self::OPT_LAST_SYNC, time(), false);
331 update_option(self::OPT_LAST_PAYLOAD_HASH, $hash, false);
332 delete_option(self::OPT_RETRY_COUNT);
333 delete_option(self::OPT_NEXT_RETRY);
334 delete_option(self::OPT_LAST_SEND_ERROR);
335 delete_transient(self::TRANSIENT_SNAPSHOT);
336 return true;
337 }
338
339 $this->record_send_failure($endpoint, $response);
340
341 $retries = (int) get_option(self::OPT_RETRY_COUNT, 0) + 1;
342 update_option(self::OPT_RETRY_COUNT, $retries, false);
343 $delay = min(86400, (int) (300 * pow(2, min($retries, 8))));
344 update_option(self::OPT_NEXT_RETRY, time() + $delay, false);
345
346 return false;
347 }
348
349 /**
350 * Last send failure detail for REST / admin (cleared on success).
351 *
352 * @return array{endpoint:string,time:int,code:int,wp_error:?string,body:?string}|null
353 */
354 public function get_last_send_error(): ?array
355 {
356 $raw = get_option(self::OPT_LAST_SEND_ERROR, null);
357 if (!is_array($raw)) {
358 return null;
359 }
360
361 return $raw;
362 }
363
364 /**
365 * @param \WP_Error|array<string,mixed> $response
366 */
367 private function record_send_failure(string $endpoint, $response): void
368 {
369 $code = is_wp_error($response) ? 0 : (int) wp_remote_retrieve_response_code($response);
370 $wpErr = is_wp_error($response) ? $response->get_error_message() : null;
371 $body = null;
372 if (!is_wp_error($response)) {
373 $b = wp_remote_retrieve_body($response);
374 if (is_string($b) && $b !== '') {
375 $body = function_exists('mb_substr') ? mb_substr($b, 0, 800) : substr($b, 0, 800);
376 }
377 }
378
379 update_option(
380 self::OPT_LAST_SEND_ERROR,
381 [
382 'endpoint' => $endpoint,
383 'time' => time(),
384 'code' => $code,
385 'wp_error' => $wpErr,
386 'body' => $body,
387 ],
388 false
389 );
390 }
391
392 /**
393 * @return array<string,mixed>
394 */
395 public function build_payload(): array
396 {
397 $system = $this->collect_system();
398 $free = $this->collect_yatra_free();
399 $pro = apply_filters('yatra_pro_usage_tracking_payload', $this->collect_yatra_pro_base());
400 $support = $this->collect_support_intel($system, $free, $pro);
401 $events = $this->get_event_counters();
402
403 $pro_arr = is_array($pro) ? $pro : [];
404 $is_pro_site = !empty($pro_arr['yatra_pro_active']) || !empty($pro_arr['yatra_pro_license_active']);
405
406 $themeRows = $this->collect_active_wordpress_themes();
407
408 $payload = [
409 'schema_version' => 2,
410 'product' => 'yatra',
411 'plugin_slug' => 'yatra',
412 'plugin_name' => 'Yatra',
413 'plugin_category' => 'booking',
414 'plugin_version' => defined('YATRA_VERSION') ? (string) YATRA_VERSION : '',
415 'is_premium' => $is_pro_site,
416 'sent_at' => gmdate('c'),
417 'blog_id' => is_multisite() ? get_current_blog_id() : 1,
418 'system' => $system,
419 'yatra_free' => $free,
420 'yatra_pro' => $pro_arr,
421 'support' => $support,
422 'events' => $events,
423 /** Full inventory for telemetry warehouse (plugins/themes/modules). */
424 'active_plugins' => $this->collect_active_wordpress_plugins(),
425 'active_theme' => $themeRows[0] ?? null,
426 'active_themes' => $themeRows,
427 'yatra_modules' => $this->collect_yatra_modules_rows(),
428 ];
429
430 return apply_filters('yatra_usage_tracking_payload', $payload);
431 }
432
433 public function ensure_instance_id(): string
434 {
435 $id = (string) get_option(self::OPT_INSTANCE_ID, '');
436 if ($id !== '') {
437 return $id;
438 }
439 if (function_exists('wp_generate_uuid4')) {
440 $id = wp_generate_uuid4();
441 } else {
442 $id = bin2hex(random_bytes(16));
443 }
444 update_option(self::OPT_INSTANCE_ID, $id, false);
445
446 return $id;
447 }
448
449 public function clear_local_cache(): void
450 {
451 delete_transient(self::TRANSIENT_SNAPSHOT);
452 delete_option(self::OPT_LAST_PAYLOAD_HASH);
453 delete_option(self::OPT_EVENT_COUNTERS);
454 }
455
456 public function delete_snapshots(): void
457 {
458 $this->clear_local_cache();
459 delete_option(self::OPT_LAST_SYNC);
460 }
461
462 /**
463 * @return array<string,int>
464 */
465 public function get_event_counters(): array
466 {
467 $c = get_option(self::OPT_EVENT_COUNTERS, []);
468 if (!is_array($c)) {
469 return [];
470 }
471 $out = [];
472 foreach ($c as $k => $v) {
473 $out[sanitize_key((string) $k)] = (int) $v;
474 }
475
476 return $out;
477 }
478
479 public function get_next_scheduled(): int
480 {
481 $t = wp_next_scheduled(self::CRON_HOOK);
482
483 return $t ? (int) $t : 0;
484 }
485
486 /**
487 * @return array<string,mixed>
488 */
489 private function collect_system(): array
490 {
491 global $wpdb;
492
493 $theme = wp_get_theme();
494 $active_plugins = (array) get_option('active_plugins', []);
495
496 return [
497 'instance_id' => $this->ensure_instance_id(),
498 'site_url' => untrailingslashit(site_url()),
499 'wp_version' => $GLOBALS['wp_version'] ?? '',
500 'php_version' => PHP_VERSION,
501 'mysql_version' => isset($wpdb->dbh) ? (string) $wpdb->db_version() : '',
502 'web_server' => isset($_SERVER['SERVER_SOFTWARE']) ? sanitize_text_field(wp_unslash((string) $_SERVER['SERVER_SOFTWARE'])) : '',
503 'ssl_enabled' => is_ssl(),
504 'timezone' => (string) wp_timezone_string(),
505 'locale' => get_locale(),
506 'multisite' => is_multisite(),
507 'wp_cron_disabled' => defined('DISABLE_WP_CRON') && DISABLE_WP_CRON,
508 'object_cache_enabled' => function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache(),
509 'active_theme_name' => (string) $theme->get('Name'),
510 'active_theme_version' => (string) $theme->get('Version'),
511 /** Aggregate only — avoids sending full stack / per-plugin versions. */
512 'active_plugin_count' => count($active_plugins),
513 ];
514 }
515
516 /**
517 * @return array<string,mixed>
518 */
519 private function collect_yatra_free(): array
520 {
521 $meta = $this->get_onboarding_meta();
522 $tours = $this->count_trips();
523 $destinations = $this->count_destinations();
524 $bookings = $this->count_bookings();
525 $gateways = SettingsService::get('payment_gateways', []);
526 if (!is_array($gateways)) {
527 $gateways = [];
528 }
529 $enabled_gateways = array_filter($gateways);
530
531 return [
532 'yatra_free_version' => defined('YATRA_VERSION') ? YATRA_VERSION : '',
533 'booking_flow' => (string) apply_filters('yatra_usage_booking_flow', 'pageless'),
534 'onboarding_started' => !empty($meta['onboarding_started']),
535 'onboarding_completed' => !empty($meta['onboarding_completed']),
536 'onboarding_last_step' => (string) ($meta['last_step'] ?? ''),
537 'setup_dropoff_step' => (string) ($meta['dropoff_step'] ?? ''),
538 'tours_count' => $tours,
539 'destinations_count' => $destinations,
540 'bookings_count' => $bookings,
541 'enquiry_forms_enabled' => (bool) get_option('yatra_enable_enquiry', false),
542 'payment_gateways_enabled' => array_keys($enabled_gateways),
543 'email_templates_customized' => $this->detect_email_templates_customized(),
544 'blocks_used' => $this->detect_yatra_blocks_used(),
545 'widgets_used' => $this->detect_yatra_widgets_used(),
546 'elementor_widgets_used' => (bool) apply_filters('yatra_usage_elementor_widgets_used', false),
547 'rest_api_usage_enabled' => (bool) apply_filters('yatra_usage_rest_api_enabled', true),
548 'beta_features_enabled' => (bool) apply_filters('yatra_usage_beta_features_enabled', false),
549 'first_trip_created' => get_option('yatra_usage_flag_first_trip', '') === '1',
550 'first_booking_received' => get_option('yatra_usage_flag_first_booking', '') === '1',
551 'upgrade_cta_clicks' => (int) ($this->get_event_counters()['pro_cta_clicked'] ?? 0),
552 'upgrade_page_views' => (int) ($this->get_event_counters()['pro_upgrade_page_visited'] ?? 0),
553 'abandoned_setup_detected' => $this->detect_abandoned_setup($meta),
554 ];
555 }
556
557 /**
558 * @return array<string,mixed>
559 */
560 private function collect_yatra_pro_base(): array
561 {
562 $pro_file = WP_PLUGIN_DIR . '/yatra-pro/yatra-pro.php';
563 $installed = file_exists($pro_file);
564 $active = (bool) apply_filters('yatra_is_pro_active', false);
565 $version = defined('YATRA_PRO_VERSION') ? (string) YATRA_PRO_VERSION : '';
566 $activated_at = (int) get_option('yatra_usage_pro_activated_at', 0);
567 if ($active && $activated_at === 0) {
568 $activated_at = time();
569 update_option('yatra_usage_pro_activated_at', $activated_at, false);
570 }
571 $days_since = $activated_at > 0 ? (int) floor((time() - $activated_at) / DAY_IN_SECONDS) : 0;
572
573 $modules = [];
574 if ($active && function_exists('get_option')) {
575 $enabled = get_option('yatra_pro_modules_enabled', []);
576 $modules = is_array($enabled) ? array_values(array_map('sanitize_key', $enabled)) : [];
577 }
578
579 return [
580 'yatra_pro_installed' => $installed,
581 'yatra_pro_active' => $active,
582 'yatra_pro_version' => $version,
583 'yatra_pro_license_active' => (bool) apply_filters('yatra_pro_usage_license_active', false),
584 'yatra_pro_license_tier' => (string) apply_filters('yatra_pro_usage_license_tier', ''),
585 'yatra_pro_days_since_activation' => $days_since,
586 'yatra_pro_last_seen' => time(),
587 /** List of enabled Pro module slugs (compact vs. all-true map). */
588 'enabled_pro_modules' => $modules,
589 'premium_payment_gateways' => (array) apply_filters('yatra_pro_usage_premium_gateways', []),
590 'coupons_enabled' => (bool) apply_filters('yatra_pro_usage_coupons_enabled', false),
591 'recurring_tours_enabled' => (bool) apply_filters('yatra_pro_usage_recurring_tours', false),
592 'seasonal_pricing_enabled' => (bool) apply_filters('yatra_pro_usage_seasonal_pricing', false),
593 'advanced_search_enabled' => (bool) apply_filters('yatra_pro_usage_advanced_search', false),
594 'partial_payment_enabled' => (bool) SettingsService::get('partial_payment', false),
595 'multicurrency_enabled' => (bool) apply_filters('yatra_pro_usage_multicurrency', false),
596 'custom_checkout_fields_enabled' => (bool) apply_filters('yatra_pro_usage_custom_checkout_fields', false),
597 'premium_email_automation_enabled' => (bool) apply_filters('yatra_pro_usage_email_automation', false),
598 'pdf_invoice_enabled' => (bool) apply_filters('yatra_pro_usage_pdf_invoice', false),
599 'woocommerce_bridge_enabled' => (bool) apply_filters('yatra_pro_usage_woocommerce', false),
600 'advanced_itinerary_builder_used' => (bool) apply_filters('yatra_pro_usage_itinerary_builder', false),
601 'abandoned_booking_recovery_enabled' => (bool) apply_filters('yatra_pro_usage_abandoned_recovery', false),
602 'agent_vendor_module_enabled' => (bool) apply_filters('yatra_pro_usage_agent_vendor', false),
603 'premium_analytics_enabled' => (bool) apply_filters('yatra_pro_usage_premium_analytics', false),
604 'premium_rest_integrations_enabled' => (bool) apply_filters('yatra_pro_usage_rest_integrations', false),
605 'premium_widgets_used' => (bool) apply_filters('yatra_pro_usage_premium_widgets', false),
606 'first_premium_booking_received' => (bool) apply_filters('yatra_pro_usage_first_premium_booking', false),
607 'unused_premium_modules_count' => (int) apply_filters('yatra_pro_usage_unused_modules_count', 0),
608 'free_pro_version_mismatch' => $this->version_mismatch($version),
609 'legacy_pro_upgrade_needed' => (bool) apply_filters('yatra_pro_usage_legacy_upgrade_needed', false),
610 'renewal_due_in_days' => (int) apply_filters('yatra_pro_usage_renewal_due_days', -1),
611 'expired_license_days' => (int) apply_filters('yatra_pro_usage_expired_license_days', 0),
612 'pro_retention_health_score' => (float) apply_filters('yatra_pro_usage_retention_health_score', 0.0),
613 ];
614 }
615
616 /**
617 * @param array<string,mixed> $system
618 * @param array<string,mixed> $free
619 * @param array<string,mixed> $pro
620 * @return array<string,mixed>
621 */
622 private function collect_support_intel(array $system, array $free, array $pro): array
623 {
624 unset($free, $pro);
625 $php_ver = PHP_VERSION;
626 $wp_ver = (string) ($system['wp_version'] ?? '');
627 $mem = $this->parse_bytes((string) ini_get('memory_limit'));
628
629 $out = [
630 'recent_cron_failures_count' => (int) get_option('yatra_usage_cron_failures', 0),
631 'recent_rest_errors_count' => (int) get_option('yatra_usage_rest_errors', 0),
632 'plugin_conflict_candidates' => $this->conflict_plugin_slugs(),
633 'low_memory_risk' => $mem > 0 && $mem < 96 * 1024 * 1024,
634 'old_php_risk' => version_compare($php_ver, '8.0', '<'),
635 'unsupported_wp_risk' => $wp_ver !== '' && version_compare($wp_ver, '6.0', '<'),
636 'checkout_misconfiguration_detected' => (bool) apply_filters('yatra_usage_checkout_misconfigured', false),
637 ];
638
639 return apply_filters('yatra_usage_support_intel_payload', $out, $system);
640 }
641
642 /**
643 * @return array<string,mixed>
644 */
645 private function detect_abandoned_setup(array $meta): bool
646 {
647 if (!empty($meta['onboarding_completed'])) {
648 return false;
649 }
650 $started = (int) ($meta['started_at'] ?? 0);
651 if ($started <= 0) {
652 return false;
653 }
654
655 return (time() - $started) > 7 * DAY_IN_SECONDS;
656 }
657
658 private function version_mismatch(string $pro_version): bool
659 {
660 if ($pro_version === '') {
661 return false;
662 }
663 $free = defined('YATRA_VERSION') ? (string) YATRA_VERSION : '';
664
665 return $free !== '' && version_compare(explode('-', $free)[0], explode('-', $pro_version)[0], '!=');
666 }
667
668 /**
669 * @return list<string>
670 */
671 private function conflict_plugin_slugs(): array
672 {
673 $candidates = [
674 'woocommerce/woocommerce.php',
675 'easy-digital-downloads/easy-digital-downloads.php',
676 'wp-travel/wp-travel.php',
677 'wp-travel-engine/wp-travel-engine.php',
678
679 ];
680 $active = (array) get_option('active_plugins', []);
681 $hit = [];
682 foreach ($candidates as $p) {
683 if (in_array($p, $active, true)) {
684 $hit[] = $p;
685 }
686 }
687
688 return apply_filters('yatra_usage_conflict_plugin_candidates', $hit);
689 }
690
691 private function parse_bytes(string $val): int
692 {
693 $val = trim($val);
694 if ($val === '' || $val === '-1') {
695 return 0;
696 }
697 $u = strtoupper(substr($val, -1));
698 $n = (float) $val;
699 if ($u === 'G') {
700 return (int) ($n * 1024 * 1024 * 1024);
701 }
702 if ($u === 'M') {
703 return (int) ($n * 1024 * 1024);
704 }
705 if ($u === 'K') {
706 return (int) ($n * 1024);
707 }
708
709 return (int) $n;
710 }
711
712 private function count_trips(): int
713 {
714 if (!class_exists(TripsTable::class)) {
715 return 0;
716 }
717 global $wpdb;
718 $table = TripsTable::getTableName();
719 $like = $wpdb->esc_like($table);
720 $exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $like));
721 if ($exists !== $table) {
722 return 0;
723 }
724 $n = $wpdb->get_var(
725 "SELECT COUNT(*) FROM `{$table}` WHERE deleted_at IS NULL OR deleted_at = '0000-00-00 00:00:00'" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
726 );
727
728 return (int) $n;
729 }
730
731 private function count_destinations(): int
732 {
733 if (!class_exists(ClassificationsTable::class)) {
734 return 0;
735 }
736 global $wpdb;
737 $table = ClassificationsTable::getTableName();
738 $like = $wpdb->esc_like($table);
739 $exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $like));
740 if ($exists !== $table) {
741 return 0;
742 }
743 $n = $wpdb->get_var(
744 $wpdb->prepare(
745 "SELECT COUNT(*) FROM `{$table}` WHERE `type` = %s AND `status` = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
746 'destination',
747 'publish'
748 )
749 );
750
751 return (int) $n;
752 }
753
754 private function count_bookings(): int
755 {
756 if (!class_exists(BookingsTable::class)) {
757 return 0;
758 }
759 global $wpdb;
760 $table = BookingsTable::getTableName();
761 $like = $wpdb->esc_like($table);
762 $exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $like));
763 if ($exists !== $table) {
764 return 0;
765 }
766 $n = $wpdb->get_var("SELECT COUNT(*) FROM `{$table}`"); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
767
768 return (int) $n;
769 }
770
771 private function detect_email_templates_customized(): bool
772 {
773 $keys = [
774 'email_template_booking_custom',
775 'email_body_booking',
776 ];
777 foreach ($keys as $k) {
778 $v = get_option('yatra_' . $k, null);
779 if ($v !== null && $v !== '') {
780 return true;
781 }
782 }
783
784 return (bool) apply_filters('yatra_usage_email_templates_customized', false);
785 }
786
787 private function detect_yatra_blocks_used(): bool
788 {
789 $detected = false;
790 if (apply_filters('yatra_usage_skip_blocks_scan', false)) {
791 return (bool) apply_filters('yatra_usage_blocks_used', $detected);
792 }
793 global $wpdb;
794 $like = '%wp:yatra/%';
795 $n = (int) $wpdb->get_var(
796 $wpdb->prepare(
797 "SELECT COUNT(ID) FROM {$wpdb->posts} WHERE post_status = 'publish' AND post_content LIKE %s LIMIT 1",
798 $like
799 )
800 );
801 $detected = $n > 0;
802
803 return (bool) apply_filters('yatra_usage_blocks_used', $detected);
804 }
805
806 private function detect_yatra_widgets_used(): bool
807 {
808 $sidebars = get_option('sidebars_widgets', []);
809 if (!is_array($sidebars)) {
810 return false;
811 }
812 foreach ($sidebars as $widgets) {
813 if (!is_array($widgets)) {
814 continue;
815 }
816 foreach ($widgets as $id) {
817 if (is_string($id) && stripos($id, 'yatra') !== false) {
818 return true;
819 }
820 }
821 }
822
823 return false;
824 }
825
826 /**
827 * All active regular + network + must-use plugins (Yatra core row excluded; sent as main product).
828 *
829 * @return list<array<string,mixed>>
830 */
831 private function collect_active_wordpress_plugins(): array
832 {
833 if (!function_exists('get_plugins')) {
834 require_once ABSPATH . 'wp-admin/includes/plugin.php';
835 }
836
837 $plugins = get_plugins();
838 $active = (array) get_option('active_plugins', []);
839
840 if (is_multisite()) {
841 $network = get_site_option('active_sitewide_plugins', []);
842 if (is_array($network)) {
843 $active = array_values(array_unique(array_merge($active, array_keys($network))));
844 }
845 }
846
847 $yatraFile = defined('YATRA_PLUGIN_FILE') ? plugin_basename((string) YATRA_PLUGIN_FILE) : 'yatra/yatra.php';
848 $metricDate = gmdate('Y-m-d');
849 $out = [];
850
851 foreach ($active as $rel) {
852 if (!is_string($rel) || !isset($plugins[$rel])) {
853 continue;
854 }
855 if ($rel === $yatraFile) {
856 continue;
857 }
858 $row = $plugins[$rel];
859 $dir = dirname($rel);
860 $slug = ($dir === '.' || $dir === '') ? basename($rel, '.php') : $dir;
861 $slug = sanitize_title(str_replace(['/', '\\'], '-', $slug));
862
863 $out[] = [
864 'product_slug' => $slug !== '' ? $slug : 'plugin',
865 'product_name' => (string) ($row['Name'] ?? $slug),
866 'product_version' => (string) ($row['Version'] ?? ''),
867 'parameters' => [
868 [
869 'parameter_id' => 'plugin_file',
870 'parameter_name' => 'Plugin file',
871 'value' => $rel,
872 'metric_date' => $metricDate,
873 ],
874 ],
875 ];
876 }
877
878 if (function_exists('get_mu_plugins')) {
879 foreach (get_mu_plugins() as $rel => $row) {
880 if (!is_array($row)) {
881 continue;
882 }
883 $base = basename($rel, '.php');
884 $slug = 'mu-' . sanitize_title($base);
885
886 $out[] = [
887 'product_slug' => $slug !== '' ? $slug : 'mu-plugin',
888 'product_name' => (string) ($row['Name'] ?? $base),
889 'product_version' => (string) ($row['Version'] ?? ''),
890 'parameters' => [
891 [
892 'parameter_id' => 'must_use',
893 'parameter_name' => 'Must-use plugin',
894 'value' => '1',
895 'metric_date' => $metricDate,
896 ],
897 [
898 'parameter_id' => 'plugin_file',
899 'parameter_name' => 'Plugin file',
900 'value' => $rel,
901 'metric_date' => $metricDate,
902 ],
903 ],
904 ];
905 }
906 }
907
908 return $out;
909 }
910
911 /**
912 * Active stylesheet theme and parent theme (if child theme).
913 *
914 * @return list<array<string,mixed>>
915 */
916 private function collect_active_wordpress_themes(): array
917 {
918 $theme = wp_get_theme();
919 $metricDate = gmdate('Y-m-d');
920 $stylesheet = (string) $theme->get_stylesheet();
921 $slug = sanitize_title($stylesheet);
922
923 $out = [
924 [
925 'product_slug' => $slug !== '' ? $slug : 'theme',
926 'product_name' => (string) $theme->get('Name'),
927 'product_version' => (string) $theme->get('Version'),
928 'product_type' => 'theme',
929 'parameters' => [
930 [
931 'parameter_id' => 'theme_role',
932 'parameter_name' => 'Theme role',
933 'value' => 'active',
934 'metric_date' => $metricDate,
935 ],
936 [
937 'parameter_id' => 'stylesheet',
938 'parameter_name' => 'Stylesheet',
939 'value' => $stylesheet,
940 'metric_date' => $metricDate,
941 ],
942 ],
943 ],
944 ];
945
946 $parent = $theme->parent();
947 if ($parent instanceof \WP_Theme) {
948 $pStylesheet = (string) $parent->get_stylesheet();
949 $pSlug = sanitize_title($pStylesheet);
950 $out[] = [
951 'product_slug' => $pSlug !== '' ? $pSlug : 'parent-theme',
952 'product_name' => (string) $parent->get('Name'),
953 'product_version' => (string) $parent->get('Version'),
954 'product_type' => 'theme',
955 'parameters' => [
956 [
957 'parameter_id' => 'theme_role',
958 'parameter_name' => 'Theme role',
959 'value' => 'parent',
960 'metric_date' => $metricDate,
961 ],
962 [
963 'parameter_id' => 'stylesheet',
964 'parameter_name' => 'Stylesheet',
965 'value' => $pStylesheet,
966 'metric_date' => $metricDate,
967 ],
968 ],
969 ];
970 }
971
972 return $out;
973 }
974
975 /**
976 * Yatra feature modules (enabled / availability flags).
977 *
978 * @return list<array<string,mixed>>
979 */
980 private function collect_yatra_modules_rows(): array
981 {
982 if (!class_exists(ModuleManager::class)) {
983 return [];
984 }
985
986 $metricDate = gmdate('Y-m-d');
987 $rows = [];
988 foreach (ModuleManager::getModules() as $mod) {
989 if (!is_array($mod) || empty($mod['slug'])) {
990 continue;
991 }
992 $slug = sanitize_key((string) $mod['slug']);
993 if ($slug === '') {
994 continue;
995 }
996 $name = $mod['name'] ?? $slug;
997 if (!is_string($name)) {
998 $name = (string) $slug;
999 }
1000
1001 $rows[] = [
1002 'product_slug' => 'yatra-module-' . $slug,
1003 'product_name' => $name,
1004 'product_type' => 'module',
1005 'parent_slug' => 'yatra',
1006 'product_version' => '',
1007 'parameters' => [
1008 [
1009 'parameter_id' => 'module_enabled',
1010 'parameter_name' => 'Enabled',
1011 'value' => !empty($mod['enabled']) ? '1' : '0',
1012 'metric_date' => $metricDate,
1013 ],
1014 [
1015 'parameter_id' => 'module_available',
1016 'parameter_name' => 'Available',
1017 'value' => !empty($mod['is_available']) ? '1' : '0',
1018 'metric_date' => $metricDate,
1019 ],
1020 [
1021 'parameter_id' => 'requires_pro',
1022 'parameter_name' => 'Requires Pro',
1023 'value' => !empty($mod['requires_pro']) ? '1' : '0',
1024 'metric_date' => $metricDate,
1025 ],
1026 ],
1027 ];
1028 }
1029
1030 return $rows;
1031 }
1032 }
1033