#!/usr/bin/env php 10, 'status' => 8, 'method' => 8]; const FLEX_WIDTH_RATIOS = ['identity' => 0.25, 'req_url' => 0.50, 'ref_view' => 0.25]; const COLORS = [ 'green' => "\033[1;32m", 'cyan' => "\033[1;36m", 'yellow' => "\033[1;33m", 'red' => "\033[1;31m", 'magenta' => "\033[1;35m", 'blue' => "\033[0;34m", 'dim' => "\033[2m", 'reset' => "\033[0m" ]; // --- Global State --- $pdo = null; $sessionCache = []; $logBuffer = []; $columnWidths = []; $displayableRows = 0; $simpleMode = false; // --- Core & Helper Functions --- function get_terminal_dimensions(): array { $width = (int)@shell_exec('tput cols'); $height = (int)@shell_exec('tput lines'); if ($width > 0 && $height > 0) { return ['width' => $width, 'height' => $height]; } // Fallback for environments where tput fails return ['width' => 160, 'height' => 40]; } function calculate_layout_sizes(): void { global $columnWidths, $displayableRows; $dims = get_terminal_dimensions(); $terminalWidth = $dims['width']; $terminalHeight = $dims['height']; $displayableRows = $terminalHeight - 5; $fixedTotalWidth = array_sum(FIXED_WIDTH_COLS); $numCols = count(FIXED_WIDTH_COLS) + count(FLEX_WIDTH_RATIOS); $borderWidth = $numCols + 1; $flexTotalWidth = $terminalWidth - $fixedTotalWidth - $borderWidth; $columnWidths = FIXED_WIDTH_COLS; foreach (FLEX_WIDTH_RATIOS as $name => $ratio) { $columnWidths[$name] = (int)floor($flexTotalWidth * $ratio); } } function db_connect(): ?PDO { $dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4"; try { return new PDO($dsn, DB_USER, DB_PASS, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]); } catch (PDOException $e) { return null; } } function getIdentityFromSession(string $sessionId): ?array { global $pdo, $sessionCache; $now = time(); if (isset($sessionCache[$sessionId]) && ($now - $sessionCache[$sessionId]['timestamp'] < SESSION_CACHE_TTL)) { return $sessionCache[$sessionId]['data']; } try { $stmt = $pdo->prepare("SELECT w.name, a.company FROM Worker w LEFT JOIN Address a ON w.address_id = a.id WHERE w.sessionid = ? LIMIT 1"); $stmt->execute([$sessionId]); $result = $stmt->fetch(); $sessionCache[$sessionId] = ['data' => $result ?: null, 'timestamp' => $now]; return $result ?: null; } catch (PDOException $e) { return null; } } function getControllerFromUrl(string $url): string { if ($url === '[direct]' || $url === '-') return '[direct]'; $path = parse_url($url, PHP_URL_PATH); if (empty($path) || $path === '/') return '[root]'; $parts = explode('/', trim($path, '/')); return ucfirst($parts[0]); } function getStatusColor(int $statusCode): string { if ($statusCode >= 500) return 'red'; if ($statusCode >= 400) return 'yellow'; if ($statusCode >= 300) return 'cyan'; return 'green'; } function parseLogLine(string $line): ?array { if (!preg_match(LOG_REGEX, $line, $matches)) return null; $url = $matches[4]; $ext = strtolower(pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION)); if (in_array($ext, IGNORED_EXTENSIONS, true)) return null; $ts = DateTime::createFromFormat('d/M/Y:H:i:s O', $matches[2]); return ["ip" => $matches[1], "timestamp" => $ts ? $ts->format('H:i:s') : '??:??:??', "method" => $matches[3], "url" => $url, "status" => (int)$matches[5], "referrer" => $matches[6], "sessionid" => $matches[7]]; } // --- UI Drawing Functions --- function draw_divider(string $left, string $mid, string $right): void { global $columnWidths; echo COLORS['dim']; $cols = ['identity', 'time', 'status', 'method', 'req_url', 'ref_view']; echo $left; foreach ($cols as $i => $key) { echo str_repeat('─', $columnWidths[$key]); echo ($i < count($cols) - 1) ? $mid : ''; } echo $right . PHP_EOL . COLORS['reset']; } /** * A fully multi-byte aware function to format cell content with padding, truncation, and color. */ function format_cell(string $text, int $width, string $color = ''): string { $text = ' ' . $text; // Add leading space // mb_strimwidth is width-aware, not just character count-aware $visible_width = mb_strwidth($text, 'UTF-8'); if ($visible_width > $width) { // Truncate and add "..." suffix $text = mb_strimwidth($text, 0, $width - 4, ' ...', 'UTF-8'); } // Manual padding because str_pad is not multi-byte safe $final_width = mb_strwidth($text, 'UTF-8'); $padding = $width - $final_width; $padded_text = $text . str_repeat(' ', $padding > 0 ? $padding : 0); return ($color ? COLORS[$color] : '') . $padded_text . ($color ? COLORS['reset'] : ''); } function draw_header(string $logFile, bool $isRedraw = false): void { global $simpleMode; if (!$simpleMode && !$isRedraw) { // Only clear screen in normal mode on initial draw echo "\033[H\033[J"; } $title = basename($logFile); $mode_indicator = $simpleMode ? " [Compatibility Mode]" : ""; echo ($isRedraw ? PHP_EOL : '') . COLORS['magenta'] . " watching {$title}{$mode_indicator} | Press Ctrl+C to exit" . COLORS['reset'] . PHP_EOL; draw_divider('┌', '┬', '┐'); $headers = [' IDENTITY', ' TIME', ' STATUS', ' METHOD', ' REQUEST URL', ' FROM VIEW']; $keys = ['identity', 'time', 'status', 'method', 'req_url', 'ref_view']; echo COLORS['dim'] . '│' . COLORS['magenta']; foreach ($keys as $i => $key) { echo str_pad($headers[$i], $GLOBALS['columnWidths'][$key]) . COLORS['dim'] . '│' . COLORS['magenta']; } echo COLORS['reset'] . PHP_EOL; draw_divider('├', '┼', '┤'); } function redraw_screen(string $logFile): void { global $logBuffer; echo "\033[H\033[J"; // Move cursor to top left and clear the screen draw_header($logFile, true); foreach ($logBuffer as $index => $data) { echo COLORS['dim'] . '│'; echo format_cell($data['identity'], $GLOBALS['columnWidths']['identity'], 'yellow'); echo COLORS['dim'] . '│'; echo format_cell($data['timestamp'], $GLOBALS['columnWidths']['time']); echo COLORS['dim'] . '│'; echo format_cell($data['status'], $GLOBALS['columnWidths']['status'], getStatusColor($data['status'])); echo COLORS['dim'] . '│'; echo format_cell($data['method'], $GLOBALS['columnWidths']['method']); echo COLORS['dim'] . '│'; echo format_cell($data['url'], $GLOBALS['columnWidths']['req_url'], 'green'); echo COLORS['dim'] . '│'; echo format_cell($data['referrer_view'], $GLOBALS['columnWidths']['ref_view'], 'blue'); echo COLORS['dim'] . '│' . PHP_EOL; if (($index + 1) % ROW_SEPARATOR_INTERVAL === 0 && ($index + 1) < count($logBuffer)) { draw_divider('├', '┼', '┤'); } } draw_divider('└', '┴', '┘'); } // --- Main Application --- if (!extension_loaded('mbstring')) { die("Error: The 'mbstring' PHP extension is required. Please install it (e.g., sudo apt install php-mbstring).\n"); } if (function_exists('pcntl_signal')) { pcntl_async_signals(true); pcntl_signal(SIGINT, function () { echo "\nWatcher stopped.\n"; exit; }); } // Check for --simple compatibility mode flag $simpleMode = in_array('--simple', $argv); calculate_layout_sizes(); $logFile = $argv[1] ?? '/var/log/apache2/access.log'; if ($logFile === '--simple') $logFile = '/var/log/apache2/access.log'; if (!is_readable($logFile)) { die("Error: Log file not readable: $logFile\n"); } $pdo = db_connect(); if (!$pdo) { exit(1); } $handle = fopen($logFile, 'r'); $position = filesize($logFile); fseek($handle, $position); $lineCounter = 0; draw_header($logFile); while (true) { clearstatcache(true, $logFile); $currentSize = filesize($logFile); if ($currentSize < $position) { fseek($handle, 0); $position = 0; $logBuffer = []; } if ($currentSize > $position) { fseek($handle, $position); $newContent = fread($handle, $currentSize - $position); $position = $currentSize; $hasNewData = false; foreach (explode(PHP_EOL, trim($newContent)) as $line) { if (empty($line)) continue; $data = parseLogLine($line); if ($data === null || $data['sessionid'] === '-' || $data['method'] === 'OPTIONS') continue; $identity = getIdentityFromSession($data['sessionid']); if ($identity === null) continue; $hasNewData = true; $data['identity'] = $identity['name'] . (!empty($identity['company']) ? " ({$identity['company']})" : ''); $data['referrer_view'] = getControllerFromUrl($data['referrer']); if ($simpleMode) { // Simple mode: just print the new row echo COLORS['dim'] . '│'; echo format_cell($data['identity'], $GLOBALS['columnWidths']['identity'], 'yellow'); echo COLORS['dim'] . '│' . format_cell($data['timestamp'], $GLOBALS['columnWidths']['time']); echo COLORS['dim'] . '│' . format_cell($data['status'], $GLOBALS['columnWidths']['status'], getStatusColor($data['status'])); echo COLORS['dim'] . '│' . format_cell($data['method'], $GLOBALS['columnWidths']['method']); echo COLORS['dim'] . '│' . format_cell($data['url'], $GLOBALS['columnWidths']['req_url'], 'green'); echo COLORS['dim'] . '│' . format_cell($data['referrer_view'], $GLOBALS['columnWidths']['ref_view'], 'blue'); echo COLORS['dim'] . '│' . PHP_EOL; $lineCounter++; if ($lineCounter >= HEADER_REDRAW_INTERVAL) { draw_header($logFile, true); $lineCounter = 0; } } else { // Normal mode: add to buffer for redrawing $logBuffer[] = $data; if (count($logBuffer) > $displayableRows) { array_shift($logBuffer); } } } if ($hasNewData && !$simpleMode) { redraw_screen($logFile); } } usleep(250000); // 0.25 seconds }