| 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 |
|