- Implemented SecurityTest to validate token generation, CSRF protection, input sanitization, and rate limiting. - Created UserModelTest to ensure correct database operations for user management, including creation, updating, banning, and fetching active users. - Developed ValidationTest to verify input validation and sanitization for user IDs, nicknames, messages, and API requests. - Introduced Security and Validation utility classes with methods for secure token generation, input sanitization, and comprehensive validation rules.
199 lines
5.8 KiB
PHP
199 lines
5.8 KiB
PHP
<?php
|
|
/**
|
|
* Active Viewer Model
|
|
* Handles real-time viewer tracking and activity monitoring
|
|
*/
|
|
|
|
class ActiveViewerModel
|
|
{
|
|
private $db;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->db = Database::getInstance();
|
|
}
|
|
|
|
/**
|
|
* Update or create active viewer record
|
|
*/
|
|
public function heartbeat($userId, $data = [])
|
|
{
|
|
$sql = "INSERT OR REPLACE INTO active_viewers
|
|
(user_id, nickname, ip_address, session_id, is_admin)
|
|
VALUES (?, ?, ?, ?, ?)";
|
|
|
|
$params = [
|
|
$userId,
|
|
$data['nickname'] ?? 'Anonymous',
|
|
$data['ip_address'] ?? Security::getClientIP(),
|
|
$data['session_id'] ?? session_id(),
|
|
$data['is_admin'] ?? false
|
|
];
|
|
|
|
return $this->db->insert($sql, $params);
|
|
}
|
|
|
|
/**
|
|
* Get count of active viewers (seen within last X seconds)
|
|
*/
|
|
public function getActiveCount($thresholdSeconds = 30)
|
|
{
|
|
return $this->db->fetchColumn(
|
|
"SELECT COUNT(*) FROM active_viewers WHERE last_seen >= datetime('now', '-{$thresholdSeconds} seconds')"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get list of active viewers with details
|
|
*/
|
|
public function getActiveViewers($thresholdSeconds = 30)
|
|
{
|
|
return $this->db->fetchAll(
|
|
"SELECT * FROM active_viewers WHERE last_seen >= datetime('now', '-{$thresholdSeconds} seconds') ORDER BY last_seen DESC"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Clean up inactive viewers
|
|
*/
|
|
public function cleanupInactive($thresholdSeconds = 60)
|
|
{
|
|
return $this->db->delete(
|
|
"DELETE FROM active_viewers WHERE last_seen < datetime('now', '-{$thresholdSeconds} seconds')"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Update specific viewer's last seen time
|
|
*/
|
|
public function updateLastSeen($userId)
|
|
{
|
|
return $this->db->update(
|
|
"UPDATE active_viewers SET last_seen = ? WHERE user_id = ?",
|
|
[date('Y-m-d H:i:s'), $userId]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Remove viewer from active list
|
|
*/
|
|
public function removeViewer($userId)
|
|
{
|
|
return $this->db->delete(
|
|
"DELETE FROM active_viewers WHERE user_id = ?",
|
|
[$userId]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get viewer by user ID
|
|
*/
|
|
public function getViewer($userId)
|
|
{
|
|
return $this->db->fetch(
|
|
"SELECT * FROM active_viewers WHERE user_id = ?",
|
|
[$userId]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Bulk update multiple viewers (for mass heartbeat handling)
|
|
*/
|
|
public function bulkHeartbeat($viewersData)
|
|
{
|
|
$this->db->beginTransaction();
|
|
|
|
try {
|
|
foreach ($viewersData as $viewerData) {
|
|
$userId = $viewerData['user_id'] ?? '';
|
|
if (!empty($userId)) {
|
|
$this->heartbeat($userId, $viewerData);
|
|
}
|
|
}
|
|
|
|
$this->db->commit();
|
|
return true;
|
|
} catch (Exception $e) {
|
|
$this->db->rollback();
|
|
error_log("Bulk heartbeat failed: " . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get viewer activity statistics
|
|
*/
|
|
public function getActivityStats()
|
|
{
|
|
$stats = [];
|
|
|
|
// Current active count
|
|
$stats['currently_active'] = $this->db->fetchColumn(
|
|
"SELECT COUNT(*) FROM active_viewers WHERE last_seen >= datetime('now', '-30 seconds')"
|
|
);
|
|
|
|
// Peak today
|
|
$stats['peak_today'] = $this->db->fetchColumn(
|
|
"SELECT COUNT(*) FROM active_viewers WHERE DATE(last_seen) = DATE('now')"
|
|
);
|
|
|
|
// Viewer types
|
|
$stats['admin_count'] = $this->db->fetchColumn(
|
|
"SELECT COUNT(*) FROM active_viewers WHERE is_admin = 1 AND last_seen >= datetime('now', '-30 seconds')"
|
|
);
|
|
|
|
// Recent joiners (joined in last 5 minutes)
|
|
$stats['recent_joiners'] = $this->db->fetchColumn(
|
|
"SELECT COUNT(*) FROM active_viewers WHERE last_seen >= datetime('now', '-5 minutes')"
|
|
);
|
|
|
|
// Top user agents
|
|
$stats['user_agents'] = $this->db->fetchAll(
|
|
"SELECT user_agent, COUNT(*) as count FROM active_viewers
|
|
WHERE last_seen >= datetime('now', '-1 hour')
|
|
GROUP BY user_agent
|
|
ORDER BY count DESC LIMIT 5"
|
|
);
|
|
|
|
return $stats;
|
|
}
|
|
|
|
/**
|
|
* Migrate old file-based viewer data if exists
|
|
*/
|
|
public function migrateFromFileIfNeeded()
|
|
{
|
|
$oldViewersFile = __DIR__ . '/../active_viewers.json';
|
|
|
|
if (!file_exists($oldViewersFile)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$oldData = json_decode(file_get_contents($oldViewersFile), true);
|
|
|
|
if (is_array($oldData)) {
|
|
$this->db->beginTransaction();
|
|
|
|
foreach ($oldData as $viewer) {
|
|
if (!empty($viewer['user_id'])) {
|
|
$this->heartbeat($viewer['user_id'], [
|
|
'nickname' => $viewer['nickname'] ?? 'Anonymous',
|
|
'ip_address' => $viewer['ip_address'] ?? '',
|
|
'session_id' => $viewer['session_id'] ?? '',
|
|
'is_admin' => $viewer['is_admin'] ?? false
|
|
]);
|
|
}
|
|
}
|
|
|
|
$this->db->commit();
|
|
|
|
// Backup old file and remove it
|
|
rename($oldViewersFile, $oldViewersFile . '.migrated.' . date('Y-m-d_H-i-s'));
|
|
error_log("Migrated " . count($oldData) . " viewers from file to database");
|
|
}
|
|
} catch (Exception $e) {
|
|
error_log("Migration from file failed: " . $e->getMessage());
|
|
}
|
|
}
|
|
}
|