/** users.last_seen_at kolonunu tembel ekler */ function users_last_seen_sema(): void { static $ok = false; if ($ok) return; try { db()->exec('ALTER TABLE users ADD COLUMN last_seen_at DATETIME NULL DEFAULT NULL'); } catch (Throwable $e) { // kolon zaten var } $ok = true; } /** Misafir + üye presence tablosu (sitedeki herkes). */ function online_presence_sema(): void { static $ok = false; if ($ok) { return; } try { db()->exec( "CREATE TABLE IF NOT EXISTS online_presence ( session_id VARCHAR(128) NOT NULL PRIMARY KEY, user_id INT UNSIGNED NULL, nickname VARCHAR(100) NULL, ip VARCHAR(45) NULL, ip_hash CHAR(64) NULL, path VARCHAR(255) NULL, last_seen_at DATETIME NOT NULL, KEY idx_online_presence_seen (last_seen_at), KEY idx_online_presence_user (user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" ); } catch (Throwable $e) { // yok say } try { db()->exec('ALTER TABLE online_presence ADD COLUMN ip VARCHAR(45) NULL AFTER nickname'); } catch (Throwable $e) { // kolon zaten var } $ok = true; } /** * Online eşiği (saniye). Bu süre içinde görülen ziyaretçi online sayılır. */ function online_esik_saniye(): int { return 900; // 15 dk } function online_bot_istegi_mi(): bool { $ua = (string)($_SERVER['HTTP_USER_AGENT'] ?? ''); if ($ua === '') { return true; } return (bool)preg_match( '/bot|crawl|spider|slurp|wget|curl\/|python-requests|facebookexternalhit|preview|monitoring|uptimerobot|pingdom|headless/i', $ua ); } /** Girişli kullanıcının last_seen_at değerini en fazla ~60 sn'de bir günceller */ function touch_user_last_seen(int $userId): void { if ($userId <= 0) return; $now = time(); $prev = (int)($_SESSION['last_seen_touch'] ?? 0); if ($prev > 0 && ($now - $prev) < 60) { return; } $_SESSION['last_seen_touch'] = $now; try { users_last_seen_sema(); db()->prepare('UPDATE users SET last_seen_at = NOW() WHERE id = :id') ->execute([':id' => $userId]); } catch (Throwable $e) { // sessiz } } /** * Sitedeki herkes (üye + misafir) için presence kaydı. * header.php üzerinden her HTML sayfada çağrılır. */ function touch_online_presence(): void { if (PHP_SAPI === 'cli') { return; } if (defined('SS_SKIP_SESSION') && SS_SKIP_SESSION) { return; } if (session_status() !== PHP_SESSION_ACTIVE) { return; } if (online_bot_istegi_mi()) { return; } $sid = session_id(); if ($sid === '') { return; } $now = time(); $prev = (int)($_SESSION['presence_touch'] ?? 0); if ($prev > 0 && ($now - $prev) < 45) { return; } $_SESSION['presence_touch'] = $now; $uid = !empty($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : null; $nick = null; if ($uid) { $nick = nick_norm((string)($_SESSION['nickname'] ?? '')); if ($nick === '') { $nick = null; } } $path = (string)(strtok($_SERVER['REQUEST_URI'] ?? '/', '?') ?: '/'); if (strlen($path) > 240) { $path = substr($path, 0, 240); } // Admin/cron gibi panelleri de say (gerçek ziyaretçi) $ip = (string)($_SERVER['REMOTE_ADDR'] ?? ''); if (strlen($ip) > 45) { $ip = substr($ip, 0, 45); } $salt = defined('DB_NAME') ? (string)DB_NAME : 'sporsozluk'; $ipHash = $ip !== '' ? hash('sha256', $ip . '|' . $salt) : null; try { online_presence_sema(); db()->prepare( 'INSERT INTO online_presence (session_id, user_id, nickname, ip, ip_hash, path, last_seen_at) VALUES (:s, :u, :n, :ip, :ih, :p, NOW()) ON DUPLICATE KEY UPDATE user_id = VALUES(user_id), nickname = VALUES(nickname), ip = VALUES(ip), ip_hash = VALUES(ip_hash), path = VALUES(path), last_seen_at = NOW()' )->execute([ ':s' => $sid, ':u' => $uid, ':n' => $nick, ':ip' => $ip !== '' ? $ip : null, ':ih' => $ipHash, ':p' => $path, ]); // Eski kayıtları seyrek temizle if (mt_rand(1, 40) === 1) { db()->exec('DELETE FROM online_presence WHERE last_seen_at < DATE_SUB(NOW(), INTERVAL 1 DAY)'); } } catch (Throwable $e) { // sessiz } } /** * Online ziyaretçi listesi (üye + misafir). * @return list> */ function online_kullanicilar(?int $esikSaniye = null): array { online_presence_sema(); $esik = (int)($esikSaniye ?? online_esik_saniye()); if ($esik < 60) { $esik = 60; } try { $st = db()->query( "SELECT p.session_id, p.user_id, p.nickname, p.path, p.last_seen_at, p.ip, p.ip_hash, u.is_admin, u.is_mod, u.is_banned, u.nickname AS user_nickname FROM online_presence p LEFT JOIN users u ON u.id = p.user_id WHERE p.last_seen_at >= DATE_SUB(NOW(), INTERVAL {$esik} SECOND) ORDER BY p.last_seen_at DESC LIMIT 300" ); $rows = $st ? ($st->fetchAll(PDO::FETCH_ASSOC) ?: []) : []; } catch (Throwable $e) { $rows = []; } // Presence henüz boşsa (ilk deploy) eski üye last_seen'e düş if (!$rows) { users_last_seen_sema(); $st = db()->query( "SELECT id AS user_id, nickname, email, is_admin, is_mod, is_banned, last_seen_at, last_login_at, NULL AS session_id, NULL AS path, NULL AS ip, NULL AS ip_hash, nickname AS user_nickname FROM users WHERE last_seen_at IS NOT NULL AND last_seen_at >= DATE_SUB(NOW(), INTERVAL {$esik} SECOND) ORDER BY last_seen_at DESC LIMIT 200" ); $rows = $st ? ($st->fetchAll(PDO::FETCH_ASSOC) ?: []) : []; } $out = []; foreach ($rows as $r) { $uid = isset($r['user_id']) ? (int)$r['user_id'] : 0; $nick = trim((string)($r['user_nickname'] ?? $r['nickname'] ?? '')); if ($uid > 0 && $nick === '') { $nick = 'üye #' . $uid; } $ip = trim((string)($r['ip'] ?? '')); $out[] = [ 'session_id' => $r['session_id'] ?? null, 'user_id' => $uid > 0 ? $uid : null, 'id' => $uid > 0 ? $uid : null, // geriye uyum 'nickname' => $uid > 0 ? $nick : null, 'is_guest' => $uid < 1, 'is_admin' => !empty($r['is_admin']), 'is_mod' => !empty($r['is_mod']), 'is_banned' => !empty($r['is_banned']), 'path' => $r['path'] ?? null, 'last_seen_at' => $r['last_seen_at'] ?? null, 'ip' => $ip !== '' ? $ip : null, 'ip_hash' => $r['ip_hash'] ?? null, ]; } return $out; } function online_kullanici_sayisi(?int $esikSaniye = null): int { online_presence_sema(); $esik = (int)($esikSaniye ?? online_esik_saniye()); if ($esik < 60) { $esik = 60; } try { $st = db()->query( "SELECT COUNT(*) FROM online_presence WHERE last_seen_at >= DATE_SUB(NOW(), INTERVAL {$esik} SECOND)" ); $n = $st ? (int)$st->fetchColumn() : 0; if ($n > 0) { return $n; } } catch (Throwable $e) { // fallback } users_last_seen_sema(); $st = db()->query( "SELECT COUNT(*) FROM users WHERE last_seen_at IS NOT NULL AND last_seen_at >= DATE_SUB(NOW(), INTERVAL {$esik} SECOND)" ); return $st ? (int)$st->fetchColumn() : 0; } /** last_seen_at için kısa göreli metin */ function online_gorulme_metni(?string $lastSeenAt): string { if ($lastSeenAt === null || $lastSeenAt === '') return '—'; $ts = strtotime($lastSeenAt); if ($ts === false) return '—'; $fark = max(0, time() - $ts); if ($fark < 45) return 'şimdi'; if ($fark < 3600) return (int)floor($fark / 60) . ' dk önce'; if ($fark < 86400) return (int)floor($fark / 3600) . ' saat önce'; return date('d.m.Y H:i', $ts); }