exec("CREATE TABLE IF NOT EXISTS analytics_events ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, ts INT UNSIGNED NOT NULL, day DATE NOT NULL, type VARCHAR(16) NOT NULL, sid CHAR(32) NOT NULL DEFAULT '', path VARCHAR(160) NOT NULL DEFAULT '', ref_host VARCHAR(120) NOT NULL DEFAULT '', src VARCHAR(40) NOT NULL DEFAULT '', device VARCHAR(8) NOT NULL DEFAULT '', bot TINYINT NOT NULL DEFAULT 0, from_code VARCHAR(10) NOT NULL DEFAULT '', to_code VARCHAR(10) NOT NULL DEFAULT '', train_no VARCHAR(10) NOT NULL DEFAULT '', jdate DATE NULL, n INT NOT NULL DEFAULT 0, note VARCHAR(60) NOT NULL DEFAULT '', ip_hash CHAR(16) NOT NULL DEFAULT '', KEY k_day (day), KEY k_type (type), KEY k_sid (sid), KEY k_from (from_code), KEY k_to (to_code), KEY k_train (train_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); $done = true; } function rr_session_id(): string { static $sid = null; if ($sid !== null) return $sid; $c = $_COOKIE['rr_sid'] ?? ''; if (preg_match('/^[a-f0-9]{32}$/', $c)) { $sid = $c; return $sid; } try { $sid = bin2hex(random_bytes(16)); } catch (Throwable $e) { $sid = md5(uniqid('', true)); } if (!headers_sent()) { $secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'); @setcookie('rr_sid', $sid, [ 'expires' => time() + 31536000, 'path' => '/', 'secure' => $secure, 'httponly' => true, 'samesite' => 'Lax', ]); } $_COOKIE['rr_sid'] = $sid; return $sid; } function rr_is_bot(string $ua): bool { return $ua === '' || (bool) preg_match( '/(bot|crawl|spider|slurp|bing|google|yandex|baidu|duckduck|facebookexternal|whatsapp|telegram|embed|preview|monitor|curl|wget|python-requests|headless|lighthouse)/i', $ua ); } function rr_device(string $ua): string { if (rr_is_bot($ua)) return 'bot'; return preg_match('/Mobi|Android|iPhone|iPad|iPod|Windows Phone/i', $ua) ? 'mobile' : 'desktop'; } /** * Log one event. $type = pageview | search | availability. * $d (all optional): path, src, from_code, to_code, train_no, jdate, n, note, force * The admin's own browsing is skipped (unless force) so visitor counts stay honest. */ function rr_track(string $type, array $d = []): void { try { if (empty($d['force']) && function_exists('current_user')) { $u = current_user(); if ($u && (($u['role'] ?? '') === 'admin')) return; } $pdo = db(); rr_analytics_ready($pdo); $ua = $_SERVER['HTTP_USER_AGENT'] ?? ''; $ref = $_SERVER['HTTP_REFERER'] ?? ''; $rh = $ref !== '' ? (string) (parse_url($ref, PHP_URL_HOST) ?: '') : ''; $self = (string) ($_SERVER['HTTP_HOST'] ?? ''); if ($rh !== '' && $self !== '' && stripos($rh, $self) !== false) $rh = ''; // ignore internal referrers $src = (string) ($d['src'] ?? $_GET['src'] ?? $_GET['utm_source'] ?? ''); $ip = $_SERVER['HTTP_CF_CONNECTING_IP'] ?? $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? ''; if (strpos($ip, ',') !== false) $ip = trim(explode(',', $ip)[0]); $jdate = (!empty($d['jdate']) && preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) $d['jdate'])) ? $d['jdate'] : null; $st = $pdo->prepare("INSERT INTO analytics_events (ts, day, type, sid, path, ref_host, src, device, bot, from_code, to_code, train_no, jdate, n, note, ip_hash) VALUES (?,CURDATE(),?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); $st->execute([ time(), substr($type, 0, 16), rr_session_id(), substr((string) ($d['path'] ?? ($_SERVER['REQUEST_URI'] ?? '')), 0, 160), substr($rh, 0, 120), substr($src, 0, 40), rr_device($ua), rr_is_bot($ua) ? 1 : 0, strtoupper(substr((string) ($d['from_code'] ?? ''), 0, 10)), strtoupper(substr((string) ($d['to_code'] ?? ''), 0, 10)), preg_replace('/[^0-9]/', '', (string) ($d['train_no'] ?? '')), $jdate, (int) ($d['n'] ?? 0), substr((string) ($d['note'] ?? ''), 0, 60), $ip !== '' ? substr(hash('sha256', $ip . RR_ANALYTICS_SALT), 0, 16) : '', ]); } catch (Throwable $e) { /* analytics must never break the page */ } }