diff --git a/TODO.md b/TODO.md index 95227d8..5e3dee0 100644 --- a/TODO.md +++ b/TODO.md @@ -11,6 +11,9 @@ Pending tasks have been split into topic files under [`todo/`](todo/README.md): ## Recently completed (this session) +- [x] `src/SystemController.php` — extracted all data-fetching logic from `admin/system.php` and `admin/system-fragment.php` into a dedicated controller class; centralises: system status checks (nginx, php-fpm, HTTP ping, SQLite DB, storage dir, maintenance flag) with 2-min TTL caching, PHP environment info (1-hour TTL), disk usage (5-min TTL), log file reading (`readLogTail`), nginx config reading, and the shared CSS-class classifier methods (`logLineClass`, `nginxLineClass`, `statusLabel`, `statusClass`, `humanBytes`, `diskColor`); `system.php` reduced 582→282 lines; `system-fragment.php` reduced 213→137 lines with all `frag_*`-prefixed duplicated helpers removed; both files now purely dispatch to the controller and render view templates + + - [x] `src/SearchController.php` — extracted all data-fetching logic from `public/search.php` into a dedicated controller class; `SearchController::create()` handles rate-limit enforcement (429 response + exit) and returns a ready instance; `handle()` sanitises GET params, runs all DB queries (`searchTheses`, `countSearchResults`, `getAvailableYears`, `getAllOrientations`, `getAllAPPrograms`, `getUsedTags`, `getPublishedAuthors`), builds the alphabetical author map, assembles OG/meta tags, and returns a flat view-variable array; `public/search.php` reduced from 285 lines to 162 lines (pure dispatcher + view template) diff --git a/public/admin/system-fragment.php b/public/admin/system-fragment.php index 9ee7a72..30b50eb 100644 --- a/public/admin/system-fragment.php +++ b/public/admin/system-fragment.php @@ -11,6 +11,9 @@ */ require_once __DIR__ . "/../../config/bootstrap.php"; require_once __DIR__ . '/../../src/AdminAuth.php'; +require_once APP_ROOT . '/src/Database.php'; +require_once APP_ROOT . '/src/SystemCache.php'; +require_once APP_ROOT . '/src/SystemController.php'; if (!AdminAuth::isAuthenticated()) { http_response_code(403); @@ -20,105 +23,31 @@ if (!AdminAuth::isAuthenticated()) { } // ── Validate inputs ──────────────────────────────────────────────────────── -const LOG_FILES_FRAG = [ - 'nginx_access' => ['label' => 'nginx — accès', 'path' => '/var/log/nginx/posterg_access.log'], - 'nginx_error' => ['label' => 'nginx — erreurs', 'path' => '/var/log/nginx/posterg_error.log'], - 'php_error' => ['label' => 'PHP-FPM — erreurs', 'path' => '/var/log/php8.4-fpm.log'], -]; -const ALLOWED_LINES_FRAG = [50, 100, 200, 500]; - $tab = $_GET['tab'] ?? 'nginx_access'; -if ($tab !== 'nginx_config' && !array_key_exists($tab, LOG_FILES_FRAG)) { +if ($tab !== 'nginx_config' && !array_key_exists($tab, SystemController::LOG_FILES)) { $tab = 'nginx_access'; } -$n = isset($_GET['n']) ? (int)$_GET['n'] : 100; -if (!in_array($n, ALLOWED_LINES_FRAG, true)) { +$n = isset($_GET['n']) ? (int) $_GET['n'] : 100; +if (!in_array($n, SystemController::ALLOWED_LINES, true)) { $n = 100; } header('Content-Type: text/html; charset=utf-8'); header('X-Robots-Tag: noindex'); -// ── Helpers (duplicated from system.php — small enough to inline) ────────── -function frag_readLogTail(string $logPath, int $lines, ?string &$errorMsg): ?array -{ - $errorMsg = null; - if (!function_exists('exec')) { - $errorMsg = "exec() est désactivé sur ce serveur."; - return null; - } - if (!file_exists($logPath)) { - $errorMsg = "Fichier introuvable : " . htmlspecialchars($logPath); - return null; - } - if (!is_readable($logPath)) { - $errorMsg = "Fichier non lisible (permissions insuffisantes) : " . htmlspecialchars($logPath); - return null; - } - $output = []; - $rc = 0; - exec('tail -n ' . intval($lines) . ' ' . escapeshellarg($logPath) . ' 2>/dev/null', $output, $rc); - if ($rc !== 0) { - $errorMsg = "Erreur lors de la lecture du fichier journal."; - return null; - } - return array_reverse($output); -} - -function frag_logLineClass(string $line): string -{ - if (preg_match('/\[(crit|emerg|alert)\]/', $line)) return 'log-crit'; - if (preg_match('/\[error\]/', $line)) return 'log-error'; - if (preg_match('/\[warn\]/', $line)) return 'log-warn'; - if (preg_match('/\[notice\]/', $line)) return 'log-notice'; - if (preg_match('/" [45]\d\d /', $line)) return 'log-error'; - if (preg_match('/" 3\d\d /', $line)) return 'log-notice'; - return ''; -} - -function frag_nginxLineClass(string $line): string -{ - $trimmed = ltrim($line); - if ($trimmed === '' || str_starts_with($trimmed, '#')) return 'nginx-comment'; - if (preg_match('/^\s*(location|server|upstream|events|http|geo|map|types)\b/', $line)) return 'nginx-block'; - return 'nginx-directive'; -} +// ── Build data via controller ────────────────────────────────────────────── +$_db = new Database(); +$_cache = new SystemCache($_db->getPDO()); +$_controller = new SystemController($_db, $_cache); // ── Render ───────────────────────────────────────────────────────────────── if ($tab === 'nginx_config') { - $livePath = '/etc/nginx/sites-available/posterg'; - $localPath = APP_ROOT . '/nginx/posterg.conf'; - - $lines = null; - $source = null; - $meta = null; - $error = null; - - foreach ([[$livePath, 'live'], [$localPath, 'local']] as [$path, $src]) { - if (file_exists($path) && is_readable($path)) { - $raw = file($path, FILE_IGNORE_NEW_LINES); - if ($raw !== false) { - $lines = $raw; - $source = $src; - $sz = filesize($path); - $meta = [ - 'path' => $path, - 'size' => $sz > 1048576 - ? number_format($sz / 1048576, 2) . ' MB' - : number_format($sz / 1024, 1) . ' KB', - 'mtime' => date('d/m/Y H:i:s', filemtime($path)), - ]; - break; - } - } - } - - if ($lines === null) { - $error = file_exists($livePath) - ? "Fichier non lisible (permissions insuffisantes) : " . htmlspecialchars($livePath) - : "Config live introuvable (" . htmlspecialchars($livePath) . ") et config locale introuvable (" . htmlspecialchars($localPath) . ")."; - } + $data = $_controller->getNginxConfigData(); + $lines = $data['lines']; + $source = $data['source']; + $meta = $data['meta']; + $error = $data['error']; if ($meta): ?>
@@ -150,32 +79,23 @@ if ($tab === 'nginx_config') {
$line): ?> -
$sz > 1048576 - ? number_format($sz / 1048576, 2) . ' MB' - : number_format($sz / 1024, 1) . ' KB', - 'mtime' => date('d/m/Y H:i:s', filemtime($logPath)), - ]; - } + // ── Log tab ──────────────────────────────────────────────────────────── + $data = $_controller->getLogData($tab, $n); + $logLines = $data['lines']; + $logError = $data['error']; + $logMeta = $data['meta']; ?>
- + @@ -186,7 +106,7 @@ if ($tab === 'nginx_config') {
- +
@@ -209,7 +129,7 @@ if ($tab === 'nginx_config') {
$line): ?> -
diff --git a/public/admin/system.php b/public/admin/system.php index f37287a..26ca1ac 100644 --- a/public/admin/system.php +++ b/public/admin/system.php @@ -3,359 +3,69 @@ require_once __DIR__ . "/../../config/bootstrap.php"; require_once __DIR__ . '/../../src/AdminAuth.php'; require_once APP_ROOT . '/src/Database.php'; require_once APP_ROOT . '/src/SystemCache.php'; +require_once APP_ROOT . '/src/SystemController.php'; AdminAuth::requireLogin(); $pageTitle = "Système"; -// Bootstrap cache (uses the same SQLite DB as the app) -$_db = new Database(); -$_cache = new SystemCache($_db->getPDO()); +$_db = new Database(); +$_cache = new SystemCache($_db->getPDO()); +$_controller = new SystemController($_db, $_cache); // ?refresh=1 force-busts all cached sections -$forceRefresh = isset($_GET['refresh']) && $_GET['refresh'] === '1'; -if ($forceRefresh) { - $_cache->invalidate('system_status'); - $_cache->invalidate('disk_info'); - $_cache->invalidate('php_info'); +if (isset($_GET['refresh']) && $_GET['refresh'] === '1') { + $_controller->invalidateAll(); } -// ═══════════════════════════════════════════════════════════════════════════════ -// SECTION 1 — STATUS DATA -// ═══════════════════════════════════════════════════════════════════════════════ +// ── Status / PHP / Disk data ────────────────────────────────────────────────── +$statusData = $_controller->getStatusData(); +$checks = $statusData['checks']; +$statusCached = $statusData['cached']; +$statusCacheAge = $statusData['cacheAge']; -function safeExec(string $cmd): ?string { - if (!function_exists('exec')) return null; - $output = []; - $rc = 0; - exec($cmd . ' 2>/dev/null', $output, $rc); - return $rc === 0 ? trim(implode("\n", $output)) : null; -} +$phpInfo = $_controller->getPhpInfo(); +$diskInfo = $_controller->getDiskInfo(); -function systemdStatus(string $unit): ?string { - $raw = safeExec("systemctl is-active " . escapeshellarg($unit)); - if ($raw === null) return null; - return in_array($raw, ['active', 'inactive', 'failed', 'activating', 'deactivating'], true) - ? $raw : 'unknown'; -} +$diskTotal = $diskInfo['total']; +$diskFree = $diskInfo['free']; +$diskUsed = $diskInfo['used']; +$diskPct = $diskInfo['pct']; +$diskColor = SystemController::diskColor($diskPct); -function localHttpCheck(string $url): ?array { - if (!function_exists('curl_init')) return null; - $ch = curl_init($url); - curl_setopt_array($ch, [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_NOBODY => true, - CURLOPT_TIMEOUT => 5, - CURLOPT_CONNECTTIMEOUT => 3, - CURLOPT_SSL_VERIFYPEER => false, - CURLOPT_SSL_VERIFYHOST => 0, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_MAXREDIRS => 3, - ]); - $start = microtime(true); - curl_exec($ch); - $ms = (int) round((microtime(true) - $start) * 1000); - $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); - unset($ch); - return $code > 0 ? [$code, $ms] : null; -} - -function humanBytes(int $bytes): string { - if ($bytes > 1073741824) return number_format($bytes / 1073741824, 1) . ' GB'; - if ($bytes > 1048576) return number_format($bytes / 1048576, 1) . ' MB'; - return number_format($bytes / 1024, 1) . ' KB'; -} - -function statusLabel(string $status): string { - return match($status) { - 'active' => '● En ligne', - 'inactive' => '○ Inactif', - 'failed' => '✕ Erreur', - 'warn' => '⚠ Attention', - default => '? Inconnu', - }; -} - -function statusClass(string $status): string { - return match($status) { - 'active' => 'status-ok', - 'inactive' => 'status-warn', - 'warn' => 'status-warn', - 'failed' => 'status-err', - default => 'status-unknown', - }; -} - -// ── system_status cache (2-minute TTL: systemctl + curl checks) ───────────── -$statusCacheAge = $_cache->ageSeconds('system_status'); -$checksFromCache = $_cache->get('system_status', 120); - -if ($checksFromCache !== null) { - $checks = $checksFromCache; - $statusCached = true; -} else { - $statusCached = false; - $checks = []; - - // nginx - $nginxStatus = systemdStatus('nginx'); - $nginxVersion = safeExec('nginx -v 2>&1 | head -1'); - $checks['nginx'] = [ - 'label' => 'nginx', - 'status' => $nginxStatus, - 'detail' => $nginxVersion, - ]; - - // php-fpm - $phpFpmStatus = null; - $phpFpmUnit = null; - foreach (['php8.3-fpm', 'php8.2-fpm', 'php8.1-fpm', 'php-fpm'] as $unit) { - $s = systemdStatus($unit); - if ($s !== null && $s !== 'unknown') { - $phpFpmStatus = $s; - $phpFpmUnit = $unit; - break; - } - } - $checks['php_fpm'] = [ - 'label' => 'php-fpm' . ($phpFpmUnit ? " ($phpFpmUnit)" : ''), - 'status' => $phpFpmStatus, - 'detail' => null, - ]; - - // Site HTTP ping - $siteUrl = (isset($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost') . '/'; - $httpResult = localHttpCheck($siteUrl); - $checks['site_http'] = [ - 'label' => 'Site HTTP', - 'status' => $httpResult !== null ? ($httpResult[0] < 500 ? 'active' : 'failed') : null, - 'detail' => $httpResult !== null ? "HTTP {$httpResult[0]} — {$httpResult[1]} ms" : 'curl indisponible', - ]; - - // Database (DB object already created above, reuse it) - $dbPath = $_db->getDatabasePath(); - $dbExists = file_exists($dbPath); - $dbWritable = $dbExists && is_writable($dbPath); - $dbSizeBytes = $dbExists ? filesize($dbPath) : null; - $dbSizeHuman = $dbSizeBytes !== null - ? ($dbSizeBytes > 1048576 - ? number_format($dbSizeBytes / 1048576, 1) . ' MB' - : number_format($dbSizeBytes / 1024, 1) . ' KB') - : 'N/A'; - $dbRowCount = null; - if ($dbExists) { - try { - $dbRowCount = $_db->getThesisCount(); - } catch (Throwable $e) { - $dbRowCount = null; - } - } - $checks['database'] = [ - 'label' => 'Base de données SQLite', - 'status' => $dbExists ? ($dbWritable ? 'active' : 'inactive') : 'failed', - 'detail' => $dbExists - ? ($dbRowCount !== null ? "$dbRowCount thèses — $dbSizeHuman" : "Lecture impossible — $dbSizeHuman") - : 'Fichier introuvable', - ]; - - // Storage directory - $storageDir = APP_ROOT . '/storage'; - $storageWritable = is_dir($storageDir) && is_writable($storageDir); - $bannersDir = $storageDir . '/banners'; - $coversDir = $storageDir . '/covers'; - $checks['storage'] = [ - 'label' => 'Répertoire storage', - 'status' => $storageWritable ? 'active' : ($storageDir ? 'inactive' : 'failed'), - 'detail' => $storageWritable - ? implode(' · ', array_filter([ - is_dir($bannersDir) ? ('banners/ ' . count(array_diff(scandir($bannersDir), ['.','..'])) . ' fichiers') : null, - is_dir($coversDir) ? ('covers/ ' . count(array_diff(scandir($coversDir), ['.','..'])) . ' fichiers') : null, - ])) - : 'Non accessible en écriture', - ]; - - // Maintenance mode - $maintenanceOn = file_exists(APP_ROOT . '/storage/maintenance.flag'); - $checks['maintenance'] = [ - 'label' => 'Mode maintenance', - 'status' => $maintenanceOn ? 'warn' : 'active', - 'detail' => $maintenanceOn ? 'Activé — site public inaccessible' : 'Désactivé', - ]; - - $_cache->set('system_status', $checks); - $statusCacheAge = 0; -} - -// ── php_info cache (1-hour TTL: PHP ini values don't change at runtime) ─────── -$phpInfoFromCache = $_cache->get('php_info', 3600); -if ($phpInfoFromCache !== null) { - $phpInfo = $phpInfoFromCache; -} else { - $phpInfo = [ - 'version' => PHP_VERSION, - 'sapi' => PHP_SAPI, - 'memory_limit' => ini_get('memory_limit'), - 'upload_max' => ini_get('upload_max_filesize'), - 'post_max' => ini_get('post_max_size'), - 'max_exec' => ini_get('max_execution_time') . 's', - ]; - $_cache->set('php_info', $phpInfo); -} - -// ── disk_info cache (5-minute TTL) ──────────────────────────────────────────── -$diskFromCache = $_cache->get('disk_info', 300); -if ($diskFromCache !== null) { - $diskTotal = $diskFromCache['total']; - $diskFree = $diskFromCache['free']; - $diskUsed = $diskFromCache['used']; - $diskPct = $diskFromCache['pct']; -} else { - $diskTotal = disk_total_space(APP_ROOT); - $diskFree = disk_free_space(APP_ROOT); - $diskUsed = $diskTotal - $diskFree; - $diskPct = $diskTotal > 0 ? (int) round($diskUsed / $diskTotal * 100) : 0; - $_cache->set('disk_info', [ - 'total' => $diskTotal, - 'free' => $diskFree, - 'used' => $diskUsed, - 'pct' => $diskPct, - ]); -} - -// ═══════════════════════════════════════════════════════════════════════════════ -// SECTION 2 — LOGS DATA -// ═══════════════════════════════════════════════════════════════════════════════ - -const LOG_FILES = [ - 'nginx_access' => ['label' => 'nginx — accès', 'path' => '/var/log/nginx/posterg_access.log'], - 'nginx_error' => ['label' => 'nginx — erreurs', 'path' => '/var/log/nginx/posterg_error.log'], - 'php_error' => ['label' => 'PHP-FPM — erreurs', 'path' => '/var/log/php8.4-fpm.log'], -]; - -const ALLOWED_LINES = [50, 100, 200, 500]; - -// Nginx config paths (live deployed, then local reference fallback) -const NGINX_CONFIG_LIVE = '/etc/nginx/sites-available/posterg'; -const NGINX_CONFIG_LOCAL = APP_ROOT . '/nginx/posterg.conf'; - -// Active tab: 'nginx_config', or a log key (status is now always shown above tabs) +// ── Active tab + line count ─────────────────────────────────────────────────── $activeTab = $_GET['tab'] ?? 'nginx_access'; if ($activeTab === 'status') { - // legacy URL — redirect to default log tab, status is always visible - $activeTab = 'nginx_access'; -} elseif ($activeTab !== 'nginx_config' && !array_key_exists($activeTab, LOG_FILES)) { + $activeTab = 'nginx_access'; // legacy redirect +} elseif ($activeTab !== 'nginx_config' && !array_key_exists($activeTab, SystemController::LOG_FILES)) { $activeTab = 'nginx_access'; } $selectedN = isset($_GET['n']) ? (int) $_GET['n'] : 100; -if (!in_array($selectedN, ALLOWED_LINES, true)) { +if (!in_array($selectedN, SystemController::ALLOWED_LINES, true)) { $selectedN = 100; } -/** - * Read the tail of a log file. Returns null on error, [] if empty, or string[]. - * $errorMsg is set on failure. - */ -function readLogTail(string $logPath, int $n, ?string &$errorMsg): ?array { - $errorMsg = null; - if (!function_exists('exec')) { - $errorMsg = "exec() est désactivé sur ce serveur."; - return null; - } - if (!file_exists($logPath)) { - $errorMsg = "Fichier introuvable : " . htmlspecialchars($logPath); - return null; - } - if (!is_readable($logPath)) { - $errorMsg = "Fichier non lisible (permissions insuffisantes) : " . htmlspecialchars($logPath); - return null; - } - $output = []; - $rc = 0; - exec('tail -n ' . intval($n) . ' ' . escapeshellarg($logPath) . ' 2>/dev/null', $output, $rc); - if ($rc !== 0) { - $errorMsg = "Erreur lors de la lecture du fichier journal."; - return null; - } - return array_reverse($output); // newest first -} +// ── Tab content data ────────────────────────────────────────────────────────── +$logLines = null; +$logError = null; +$logFileMeta = null; -function logLineClass(string $line): string { - if (preg_match('/\[(crit|emerg|alert)\]/', $line)) return 'log-crit'; - if (preg_match('/\[error\]/', $line)) return 'log-error'; - if (preg_match('/\[warn\]/', $line)) return 'log-warn'; - if (preg_match('/\[notice\]/', $line)) return 'log-notice'; - if (preg_match('/" [45]\d\d /', $line)) return 'log-error'; - if (preg_match('/" 3\d\d /', $line)) return 'log-notice'; - return ''; -} - -// Pre-load the active log tab if it's a log key -$logLines = null; -$logError = null; -$logFileMeta = null; - -if ($activeTab !== 'nginx_config') { - $logPath = LOG_FILES[$activeTab]['path']; - $logLines = readLogTail($logPath, $selectedN, $logError); - if (file_exists($logPath)) { - $sz = filesize($logPath); - $logFileMeta = [ - 'size' => $sz > 1048576 - ? number_format($sz / 1048576, 2) . ' MB' - : number_format($sz / 1024, 1) . ' KB', - 'mtime' => date('d/m/Y H:i:s', filemtime($logPath)), - ]; - } -} - -// Pre-load nginx config tab $nginxConfigLines = null; $nginxConfigSource = null; $nginxConfigError = null; $nginxConfigMeta = null; if ($activeTab === 'nginx_config') { - // Try live deployed config first, fall back to local reference copy - $livePath = NGINX_CONFIG_LIVE; - $localPath = NGINX_CONFIG_LOCAL; - - if (file_exists($livePath) && is_readable($livePath)) { - $raw = file($livePath, FILE_IGNORE_NEW_LINES); - if ($raw !== false) { - $nginxConfigLines = $raw; - $nginxConfigSource = 'live'; - $sz = filesize($livePath); - $nginxConfigMeta = [ - 'path' => $livePath, - 'size' => $sz > 1048576 - ? number_format($sz / 1048576, 2) . ' MB' - : number_format($sz / 1024, 1) . ' KB', - 'mtime' => date('d/m/Y H:i:s', filemtime($livePath)), - ]; - } - } - - if ($nginxConfigLines === null && file_exists($localPath) && is_readable($localPath)) { - $raw = file($localPath, FILE_IGNORE_NEW_LINES); - if ($raw !== false) { - $nginxConfigLines = $raw; - $nginxConfigSource = 'local'; - $sz = filesize($localPath); - $nginxConfigMeta = [ - 'path' => $localPath, - 'size' => $sz > 1048576 - ? number_format($sz / 1048576, 2) . ' MB' - : number_format($sz / 1024, 1) . ' KB', - 'mtime' => date('d/m/Y H:i:s', filemtime($localPath)), - ]; - } - } - - if ($nginxConfigLines === null) { - $nginxConfigError = file_exists($livePath) - ? "Fichier non lisible (permissions insuffisantes) : " . htmlspecialchars($livePath) - : "Config live introuvable (" . htmlspecialchars($livePath) . ") et config locale introuvable (" . htmlspecialchars($localPath) . ")."; - } + $nginxData = $_controller->getNginxConfigData(); + $nginxConfigLines = $nginxData['lines']; + $nginxConfigSource = $nginxData['source']; + $nginxConfigMeta = $nginxData['meta']; + $nginxConfigError = $nginxData['error']; +} else { + $logData = $_controller->getLogData($activeTab, $selectedN); + $logLines = $logData['lines']; + $logError = $logData['error']; + $logFileMeta = $logData['meta']; } $isAdmin = true; $bodyClass = 'admin-body'; @@ -404,7 +114,7 @@ require_once APP_ROOT . '/templates/head.php';
- +
@@ -427,13 +137,12 @@ require_once APP_ROOT . '/templates/head.php';

Espace disque

- 85 ? '#e05555' : ($diskPct > 70 ? '#ffc107' : '#4caf50'); ?>
- utilisé (%) - libre / + utilisé (%) + libre /
@@ -442,7 +151,7 @@ require_once APP_ROOT . '/templates/head.php';
-
$line): ?> -
@@ -524,7 +224,7 @@ require_once APP_ROOT . '/templates/head.php';