- Created complete documentation in docs/ directory - Added PROJECT_OVERVIEW.md with feature highlights and getting started guide - Added ARCHITECTURE.md with system design and technical details - Added SECURITY.md with comprehensive security implementation guide - Added DEVELOPMENT.md with development workflows and best practices - Added DEPLOYMENT.md with production deployment instructions - Added API.md with complete REST API documentation - Added CONTRIBUTING.md with contribution guidelines - Added CHANGELOG.md with version history and migration notes - Reorganized all documentation files into docs/ directory for better organization - Updated README.md with proper documentation links and quick navigation - Enhanced project structure with professional documentation standards
493 lines
17 KiB
PHP
493 lines
17 KiB
PHP
<?php
|
|
/*******************************************************************************************************************
|
|
| Software Name : EasyStream
|
|
| Software Description : High End YouTube Clone Script with Videos, Shorts, Streams, Images, Audio, Documents, Blogs
|
|
| Software Author : (c) Sami Ahmed
|
|
|*******************************************************************************************************************
|
|
|
|
|
|*******************************************************************************************************************
|
|
| This source file is subject to the EasyStream Proprietary License Agreement.
|
|
|
|
|
| By using this software, you acknowledge having read this Agreement and agree to be bound thereby.
|
|
|*******************************************************************************************************************
|
|
| Copyright (c) 2025 Sami Ahmed. All rights reserved.
|
|
|*******************************************************************************************************************/
|
|
|
|
defined('_ISVALID') or header('Location: /error');
|
|
|
|
/**
|
|
* Enhanced Logging System for Error Tracking and Debugging
|
|
*/
|
|
class VLogger
|
|
{
|
|
const EMERGENCY = 'emergency';
|
|
const ALERT = 'alert';
|
|
const CRITICAL = 'critical';
|
|
const ERROR = 'error';
|
|
const WARNING = 'warning';
|
|
const NOTICE = 'notice';
|
|
const INFO = 'info';
|
|
const DEBUG = 'debug';
|
|
|
|
private static $instance = null;
|
|
private $logPath = 'f_data/logs/';
|
|
private $maxFileSize = 10485760; // 10MB
|
|
private $maxFiles = 5;
|
|
private $context = [];
|
|
|
|
public static function getInstance()
|
|
{
|
|
if (self::$instance === null) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
private function __construct()
|
|
{
|
|
// Ensure log directory exists
|
|
if (!is_dir($this->logPath)) {
|
|
mkdir($this->logPath, 0755, true);
|
|
}
|
|
|
|
// Set global context
|
|
$this->setGlobalContext();
|
|
}
|
|
|
|
/**
|
|
* Set global context information
|
|
*/
|
|
private function setGlobalContext()
|
|
{
|
|
$this->context = [
|
|
'timestamp' => date('Y-m-d H:i:s'),
|
|
'request_id' => $this->generateRequestId(),
|
|
'ip' => $this->getClientIP(),
|
|
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
|
|
'request_uri' => $_SERVER['REQUEST_URI'] ?? '',
|
|
'request_method' => $_SERVER['REQUEST_METHOD'] ?? '',
|
|
'user_id' => $_SESSION['USER_ID'] ?? null,
|
|
'session_id' => session_id() ?: null,
|
|
'memory_usage' => memory_get_usage(true),
|
|
'peak_memory' => memory_get_peak_usage(true),
|
|
'execution_time' => microtime(true) - ($_SERVER['REQUEST_TIME_FLOAT'] ?? microtime(true))
|
|
];
|
|
|
|
// Expose request id to clients for correlation
|
|
if (!headers_sent()) {
|
|
header('X-Request-ID: ' . $this->context['request_id']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate unique request ID for tracking
|
|
*/
|
|
private function generateRequestId()
|
|
{
|
|
return uniqid('req_', true);
|
|
}
|
|
|
|
/**
|
|
* Get real client IP address
|
|
*/
|
|
private function getClientIP()
|
|
{
|
|
$ipKeys = ['HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR'];
|
|
|
|
foreach ($ipKeys as $key) {
|
|
if (array_key_exists($key, $_SERVER) === true) {
|
|
$ip = $_SERVER[$key];
|
|
if (strpos($ip, ',') !== false) {
|
|
$ip = explode(',', $ip)[0];
|
|
}
|
|
$ip = trim($ip);
|
|
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
|
|
return $ip;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
|
}
|
|
|
|
/**
|
|
* Log message with specified level
|
|
*/
|
|
public function log($level, $message, $context = [])
|
|
{
|
|
$logData = array_merge($this->context, [
|
|
'level' => $level,
|
|
'message' => $message,
|
|
'context' => $context,
|
|
'backtrace' => $this->getBacktrace()
|
|
]);
|
|
|
|
// Write to appropriate log files
|
|
$this->writeToFile($level, $logData);
|
|
|
|
// Write to database if configured
|
|
$this->writeToDatabase($level, $logData);
|
|
|
|
// Send alerts for critical errors
|
|
if (in_array($level, [self::EMERGENCY, self::ALERT, self::CRITICAL, self::ERROR])) {
|
|
$this->sendAlert($level, $logData);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get formatted backtrace
|
|
*/
|
|
private function getBacktrace()
|
|
{
|
|
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10);
|
|
$formattedTrace = [];
|
|
|
|
foreach ($trace as $i => $frame) {
|
|
if ($i === 0) continue; // Skip current function
|
|
|
|
$formattedTrace[] = [
|
|
'file' => $frame['file'] ?? 'unknown',
|
|
'line' => $frame['line'] ?? 0,
|
|
'function' => $frame['function'] ?? 'unknown',
|
|
'class' => $frame['class'] ?? null
|
|
];
|
|
}
|
|
|
|
return $formattedTrace;
|
|
}
|
|
|
|
/**
|
|
* Write log to file
|
|
*/
|
|
private function writeToFile($level, $logData)
|
|
{
|
|
$filename = $this->logPath . date('Y-m-d') . '_' . $level . '.log';
|
|
|
|
// Rotate log if too large
|
|
if (file_exists($filename) && filesize($filename) > $this->maxFileSize) {
|
|
$this->rotateLogFile($filename);
|
|
}
|
|
|
|
$logLine = json_encode($logData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n";
|
|
|
|
file_put_contents($filename, $logLine, FILE_APPEND | LOCK_EX);
|
|
}
|
|
|
|
/**
|
|
* Rotate log files
|
|
*/
|
|
private function rotateLogFile($filename)
|
|
{
|
|
for ($i = $this->maxFiles - 1; $i > 0; $i--) {
|
|
$oldFile = $filename . '.' . $i;
|
|
$newFile = $filename . '.' . ($i + 1);
|
|
|
|
if (file_exists($oldFile)) {
|
|
if ($i === $this->maxFiles - 1) {
|
|
unlink($oldFile);
|
|
} else {
|
|
rename($oldFile, $newFile);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (file_exists($filename)) {
|
|
rename($filename, $filename . '.1');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Write to database (optional)
|
|
*/
|
|
private function writeToDatabase($level, $logData)
|
|
{
|
|
global $db, $cfg;
|
|
|
|
// Only log to database if enabled in config (supports old/new keys)
|
|
$dbLogging = $cfg['logging_database_logging'] ?? ($cfg['database_logging'] ?? false);
|
|
if (!$dbLogging) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$sql = "INSERT INTO `db_logs` (`level`, `message`, `context`, `request_id`, `user_id`, `ip`, `user_agent`, `request_uri`, `created_at`)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())";
|
|
|
|
$db->Execute($sql, [
|
|
$level,
|
|
$logData['message'],
|
|
json_encode($logData['context']),
|
|
$logData['request_id'],
|
|
$logData['user_id'],
|
|
$logData['ip'],
|
|
$logData['user_agent'] ?? null,
|
|
$logData['request_uri'] ?? null
|
|
]);
|
|
} catch (Exception $e) {
|
|
// Fallback to file logging if database fails
|
|
error_log("Database logging failed: " . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send alerts for critical errors
|
|
*/
|
|
private function sendAlert($level, $logData)
|
|
{
|
|
global $cfg;
|
|
|
|
// Only send alerts if configured
|
|
if (!isset($cfg['error_alerts']) || !$cfg['error_alerts']) {
|
|
return;
|
|
}
|
|
|
|
// Rate limit alerts to prevent spam
|
|
$alertKey = 'alert_' . md5($logData['message']);
|
|
if (!$this->checkAlertRateLimit($alertKey)) {
|
|
return;
|
|
}
|
|
|
|
$subject = "EasyStream {$level}: " . substr($logData['message'], 0, 50);
|
|
$body = $this->formatAlertEmail($logData);
|
|
|
|
// Send email alert (implement based on your email system)
|
|
if (isset($cfg['admin_email']) && !empty($cfg['admin_email'])) {
|
|
$this->sendEmailAlert($cfg['admin_email'], $subject, $body);
|
|
}
|
|
|
|
// Optional webhook alerting
|
|
$webhookEnabled = $cfg['logging_error_webhook'] ?? false;
|
|
$webhookUrl = $cfg['logging_webhook_url'] ?? '';
|
|
$levels = $cfg['logging_webhook_levels'] ?? ['emergency','alert','critical','error'];
|
|
if ($webhookEnabled && $webhookUrl && in_array($level, $levels)) {
|
|
$this->sendWebhook($webhookUrl, $level, $logData);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check alert rate limiting
|
|
*/
|
|
private function checkAlertRateLimit($key, $maxAlerts = 5, $timeWindow = 3600)
|
|
{
|
|
// Prefer Redis counter with TTL; fallback to PHP session
|
|
$redis = $this->getRedis();
|
|
if ($redis) {
|
|
$rkey = 'alert_limit:' . $key;
|
|
try {
|
|
$cnt = $redis->incr($rkey);
|
|
if ((int)$cnt === 1) {
|
|
$redis->expire($rkey, (int)$timeWindow);
|
|
}
|
|
return (int)$cnt <= (int)$maxAlerts;
|
|
} catch (\Exception $e) {
|
|
// fallthrough to session fallback
|
|
}
|
|
}
|
|
|
|
if (!isset($_SESSION)) {
|
|
session_start();
|
|
}
|
|
$now = time();
|
|
$alertKey = 'alert_limit_' . $key;
|
|
if (!isset($_SESSION[$alertKey])) {
|
|
$_SESSION[$alertKey] = [];
|
|
}
|
|
$_SESSION[$alertKey] = array_filter($_SESSION[$alertKey], function($timestamp) use ($now, $timeWindow) {
|
|
return ($now - $timestamp) < $timeWindow;
|
|
});
|
|
if (count($_SESSION[$alertKey]) >= $maxAlerts) {
|
|
return false;
|
|
}
|
|
$_SESSION[$alertKey][] = $now;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Optional Redis client for rate limiting
|
|
*/
|
|
private function getRedis()
|
|
{
|
|
static $r = null;
|
|
if ($r === false) return null;
|
|
if ($r instanceof \Redis) return $r;
|
|
$host = getenv('REDIS_HOST') ?: ($GLOBALS['cfg']['redis_host'] ?? null);
|
|
$port = (int) (getenv('REDIS_PORT') ?: ($GLOBALS['cfg']['redis_port'] ?? 6379));
|
|
$db = (int) (getenv('REDIS_DB') ?: ($GLOBALS['cfg']['redis_db'] ?? 0));
|
|
if (!$host || !class_exists('Redis')) { $r = false; return null; }
|
|
try {
|
|
$cli = new \Redis();
|
|
if (!$cli->connect($host, $port, 1.5)) { $r = false; return null; }
|
|
if ($db) $cli->select($db);
|
|
$r = $cli;
|
|
return $cli;
|
|
} catch (\Exception $e) {
|
|
$r = false; return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send webhook with log payload
|
|
*/
|
|
private function sendWebhook($url, $level, $logData)
|
|
{
|
|
$payload = json_encode(['level' => $level, 'log' => $logData]);
|
|
$opts = [
|
|
'http' => [
|
|
'method' => 'POST',
|
|
'header' => "Content-Type: application/json\r\n",
|
|
'content' => $payload,
|
|
'timeout' => 2,
|
|
]
|
|
];
|
|
@file_get_contents($url, false, stream_context_create($opts));
|
|
}
|
|
|
|
/**
|
|
* Format alert email
|
|
*/
|
|
private function formatAlertEmail($logData)
|
|
{
|
|
$body = "Error Details:\n\n";
|
|
$body .= "Level: " . strtoupper($logData['level']) . "\n";
|
|
$body .= "Message: " . $logData['message'] . "\n";
|
|
$body .= "Request ID: " . $logData['request_id'] . "\n";
|
|
$body .= "Time: " . $logData['timestamp'] . "\n";
|
|
$body .= "IP: " . $logData['ip'] . "\n";
|
|
$body .= "User ID: " . ($logData['user_id'] ?: 'Guest') . "\n";
|
|
$body .= "URI: " . $logData['request_uri'] . "\n";
|
|
$body .= "Method: " . $logData['request_method'] . "\n";
|
|
$body .= "User Agent: " . $logData['user_agent'] . "\n\n";
|
|
|
|
if (!empty($logData['context'])) {
|
|
$body .= "Context:\n" . json_encode($logData['context'], JSON_PRETTY_PRINT) . "\n\n";
|
|
}
|
|
|
|
if (!empty($logData['backtrace'])) {
|
|
$body .= "Stack Trace:\n";
|
|
foreach ($logData['backtrace'] as $i => $frame) {
|
|
$body .= "#{$i} {$frame['file']}:{$frame['line']} ";
|
|
if ($frame['class']) {
|
|
$body .= "{$frame['class']}::{$frame['function']}()\n";
|
|
} else {
|
|
$body .= "{$frame['function']}()\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
return $body;
|
|
}
|
|
|
|
/**
|
|
* Send email alert
|
|
*/
|
|
private function sendEmailAlert($to, $subject, $body)
|
|
{
|
|
// Use your existing email system or implement basic mail
|
|
$headers = "From: noreply@" . ($_SERVER['HTTP_HOST'] ?? 'localhost') . "\r\n";
|
|
$headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
|
|
|
|
mail($to, $subject, $body, $headers);
|
|
}
|
|
|
|
// Convenience methods for different log levels
|
|
public function emergency($message, $context = []) { $this->log(self::EMERGENCY, $message, $context); }
|
|
public function alert($message, $context = []) { $this->log(self::ALERT, $message, $context); }
|
|
public function critical($message, $context = []) { $this->log(self::CRITICAL, $message, $context); }
|
|
public function error($message, $context = []) { $this->log(self::ERROR, $message, $context); }
|
|
public function warning($message, $context = []) { $this->log(self::WARNING, $message, $context); }
|
|
public function notice($message, $context = []) { $this->log(self::NOTICE, $message, $context); }
|
|
public function info($message, $context = []) { $this->log(self::INFO, $message, $context); }
|
|
public function debug($message, $context = []) { $this->log(self::DEBUG, $message, $context); }
|
|
|
|
/**
|
|
* Log database errors with query information
|
|
*/
|
|
public function logDatabaseError($error, $query = '', $params = [])
|
|
{
|
|
$this->error('Database Error: ' . $error, [
|
|
'query' => $query,
|
|
'parameters' => $params,
|
|
'database_error' => true
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Log security events
|
|
*/
|
|
public function logSecurityEvent($event, $context = [])
|
|
{
|
|
$this->warning('Security Event: ' . $event, array_merge($context, [
|
|
'security_event' => true
|
|
]));
|
|
}
|
|
|
|
/**
|
|
* Log performance issues
|
|
*/
|
|
public function logPerformanceIssue($message, $executionTime, $context = [])
|
|
{
|
|
$this->notice('Performance Issue: ' . $message, array_merge($context, [
|
|
'execution_time' => $executionTime,
|
|
'performance_issue' => true
|
|
]));
|
|
}
|
|
|
|
/**
|
|
* Get recent logs for admin dashboard
|
|
*/
|
|
public function getRecentLogs($level = null, $limit = 100, $offset = 0)
|
|
{
|
|
global $db, $cfg;
|
|
$logs = [];
|
|
|
|
$dbLogging = $cfg['logging_database_logging'] ?? ($cfg['database_logging'] ?? false);
|
|
if ($dbLogging && isset($db)) {
|
|
try {
|
|
$sql = "SELECT `level`, `message`, `context`, `request_id`, `user_id`, `ip`, `user_agent`, `request_uri`, `created_at`
|
|
FROM `db_logs`";
|
|
$params = [];
|
|
if (!empty($level)) {
|
|
$sql .= " WHERE `level` = ?";
|
|
$params[] = $level;
|
|
}
|
|
$sql .= " ORDER BY `created_at` DESC LIMIT " . (int)$limit . " OFFSET " . (int)$offset;
|
|
$res = empty($params) ? $db->Execute($sql) : $db->Execute($sql, $params);
|
|
if ($res) {
|
|
while (!$res->EOF) {
|
|
$ctx = json_decode($res->fields['context'] ?: '{}', true);
|
|
$logs[] = [
|
|
'level' => $res->fields['level'],
|
|
'message' => $res->fields['message'],
|
|
'context' => is_array($ctx) ? $ctx : [],
|
|
'request_id' => $res->fields['request_id'],
|
|
'user_id' => $res->fields['user_id'],
|
|
'ip' => $res->fields['ip'],
|
|
'user_agent' => $res->fields['user_agent'],
|
|
'request_uri' => $res->fields['request_uri'],
|
|
'timestamp' => $res->fields['created_at'],
|
|
];
|
|
$res->MoveNext();
|
|
}
|
|
}
|
|
return $logs;
|
|
} catch (\Exception $e) {}
|
|
}
|
|
|
|
// File fallback: aggregate recent lines then slice by offset/limit
|
|
$pattern = $this->logPath . date('Y-m-d') . '_' . ($level ?: '*') . '.log';
|
|
foreach (glob($pattern) as $file) {
|
|
$lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
|
foreach ($lines as $line) {
|
|
$logEntry = json_decode($line, true);
|
|
if ($logEntry) {
|
|
$logs[] = $logEntry;
|
|
}
|
|
}
|
|
}
|
|
usort($logs, function($a, $b) {
|
|
return strtotime($b['timestamp']) - strtotime($a['timestamp']);
|
|
});
|
|
return array_slice($logs, (int)$offset, (int)$limit);
|
|
}
|
|
}
|