logPath = dirname(__DIR__, 2) . '/logs'; $this->cachePath = dirname(__DIR__, 2) . '/cache'; } /** * Display system information. * * @param Request $request * @return Response */ public function info(Request $request): Response { $user = $this->getCurrentUser(); if (!$user || !$user->isSuperAdmin()) { return new Response($this->view->render('components/alerts.twig', [ 'type' => 'danger', 'message' => 'Access denied.' ]), 403); } $dbVersion = 'Unknown'; try { $pdo = DB::connection()->getPdo(); $dbVersion = $pdo->getAttribute(\PDO::ATTR_SERVER_VERSION); } catch (\Exception $e) { $dbVersion = 'Error: ' . $e->getMessage(); } $info = [ 'app' => [ 'name' => 'SysCMS', 'version' => '1.0.0', // Should be from config or const 'env' => getenv('APP_ENV') ?: 'production', 'debug' => getenv('APP_DEBUG') ?: false, ], 'system' => [ 'os' => php_uname('s') . ' ' . php_uname('r'), 'hostname' => gethostname(), 'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown', 'timezone' => date_default_timezone_get(), ], 'php' => [ 'version' => phpversion(), 'sapi' => php_sapi_name(), 'memory_limit' => ini_get('memory_limit'), 'max_execution_time' => ini_get('max_execution_time') . 's', 'upload_max_filesize' => ini_get('upload_max_filesize'), 'post_max_size' => ini_get('post_max_size'), 'extensions' => get_loaded_extensions(), ], 'database' => [ 'driver' => DB::connection()->getDriverName(), 'version' => $dbVersion, 'database' => DB::connection()->getDatabaseName(), ], 'disk' => [ 'total' => $this->formatBytes(disk_total_space('.')), 'free' => $this->formatBytes(disk_free_space('.')), 'used' => $this->formatBytes(disk_total_space('.') - disk_free_space('.')), ] ]; return $this->renderAdminView('system/info.twig', [ 'page_title' => 'System Information', 'info' => $info ]); } /** * Display PHP Info. * * @param Request $request * @return Response */ public function phpInfo(Request $request): Response { $user = $this->getCurrentUser(); if (!$user || !$user->isSuperAdmin()) { return new Response('Access denied', 403); } ob_start(); phpinfo(); $phpinfo = ob_get_clean(); // Extract body content to embed cleanly if desired, or just output raw. // For admin panel consistency, usually raw in a new tab is easiest, // but embedding provides better UX. // Let's strip body tags to embed. $phpinfo = preg_replace('%^.*(.*).*$%ms', '$1', $phpinfo); return $this->renderAdminView('system/phpinfo.twig', [ 'page_title' => 'PHP Info', 'phpinfo' => $phpinfo ]); } /** * List all log files. * * @param Request $request * @return Response */ public function logsIndex(Request $request): Response { $user = $this->getCurrentUser(); if (!$user || !$user->isSuperAdmin()) { return new Response($this->view->render('components/alerts.twig', [ 'type' => 'danger', 'message' => 'Access denied.' ]), 403); } $files = []; if (is_dir($this->logPath)) { $files = glob($this->logPath . '/*.log'); } // Sort by modification time desc usort($files, function($a, $b) { return filemtime($b) - filemtime($a); }); $logs = []; foreach ($files as $file) { $logs[] = [ 'name' => basename($file), 'size' => $this->formatBytes(filesize($file)), 'modified' => date('Y-m-d H:i:s', filemtime($file)) ]; } return $this->renderAdminView('logs/index.twig', [ 'page_title' => 'System Logs', 'logs' => $logs ]); } /** * Show log file content. * * @param Request $request * @return Response */ public function logsShow(Request $request): Response { $user = $this->getCurrentUser(); if (!$user || !$user->isSuperAdmin()) { return new Response($this->view->render('components/alerts.twig', [ 'type' => 'danger', 'message' => 'Access denied.' ]), 403); } $filename = $request->query->get('file'); if (!$filename) { return new RedirectResponse(url('admin/logs')); } // Security check: prevent directory traversal if (strpos($filename, '..') !== false || strpos($filename, '/') !== false || strpos($filename, '\\') !== false) { return new Response('Invalid filename', 400); } $filePath = $this->logPath . '/' . $filename; if (!file_exists($filePath)) { return new Response('File not found', 404); } // Limit file size read to avoid memory issues (e.g., 5MB) if (filesize($filePath) > 5 * 1024 * 1024) { // Read only last 500 lines if file is too big $content = $this->tailCustom($filePath, 500); $content = "File too large, showing last 500 lines:\n\n" . $content; } else { $content = file_get_contents($filePath); } return $this->renderAdminView('logs/show.twig', [ 'page_title' => 'Log: ' . $filename, 'filename' => $filename, 'content' => $content ]); } /** * Clear system cache. * * @param Request $request * @return Response */ public function clearCache(Request $request): Response { $user = $this->getCurrentUser(); if (!$user || !$user->isSuperAdmin()) { return new Response($this->view->render('components/alerts.twig', [ 'type' => 'danger', 'message' => 'Access denied.' ]), 403); } $deletedCount = 0; // Clear file cache if (is_dir($this->cachePath)) { $files = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($this->cachePath, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::CHILD_FIRST ); foreach ($files as $fileinfo) { // Skip .gitignore if ($fileinfo->getFilename() === '.gitignore') { continue; } $todo = ($fileinfo->isDir() ? 'rmdir' : 'unlink'); if (@$todo($fileinfo->getRealPath())) { if ($todo === 'unlink') { $deletedCount++; } } } } // Clear OpCache $opcacheCleared = false; if (function_exists('opcache_reset')) { $opcacheCleared = opcache_reset(); } // Add a success flash message (if flash system exists, otherwise just redirect with query param) // Since we don't have a flash message system clearly defined in context, let's use query param return new RedirectResponse(url('dashboard?cache_cleared=1&deleted=' . $deletedCount . '&opcache=' . ($opcacheCleared ? 1 : 0))); } private function formatBytes($bytes, $precision = 2) { $units = array('B', 'KB', 'MB', 'GB', 'TB'); $bytes = max($bytes, 0); $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); $pow = min($pow, count($units) - 1); $bytes /= pow(1024, $pow); return round($bytes, $precision) . ' ' . $units[$pow]; } /** * Slightly modified tail function */ private function tailCustom($filepath, $lines = 1) { $f = @fopen($filepath, "rb"); if ($f === false) return false; $buffer = 4096; fseek($f, -1, SEEK_END); if (fread($f, 1) != "\n") $lines -= 1; $output = ''; $chunk = ''; while (ftell($f) > 0 && $lines >= 0) { $seek = min(ftell($f), $buffer); fseek($f, -$seek, SEEK_CUR); $chunk = fread($f, $seek); $output = $chunk . $output; fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR); $lines -= substr_count($chunk, "\n"); } while ($lines++ < 0) { $output = substr($output, strpos($output, "\n") + 1); } fclose($f); return $output; } }