1<?php
2/**
3 * System Stats API Endpoint
4 * Returns real-time CPU and memory usage
5 *
6 * Requires admin authentication
7 */
8
9// Initialize DokuWiki environment for authentication
10if (!defined('DOKU_INC')) {
11    define('DOKU_INC', dirname(__FILE__) . '/../../../');
12}
13require_once(DOKU_INC . 'inc/init.php');
14
15// Require admin privileges
16if (!auth_isadmin()) {
17    header('Content-Type: application/json');
18    http_response_code(403);
19    echo json_encode(['error' => 'Admin access required']);
20    exit;
21}
22
23header('Content-Type: application/json');
24header('Cache-Control: no-cache, must-revalidate');
25
26$stats = [
27    'cpu' => 0,
28    'cpu_5min' => 0,
29    'memory' => 0,
30    'timestamp' => time(),
31    'load' => ['1min' => 0, '5min' => 0, '15min' => 0],
32    'uptime' => '',
33    'memory_details' => [],
34    'top_processes' => []
35];
36
37// Get CPU usage and load averages
38if (function_exists('sys_getloadavg')) {
39    $load = sys_getloadavg();
40    if ($load !== false) {
41        // Use 1-minute load average for real-time feel
42        // Normalize to percentage (assuming max load of 2.0 = 100%)
43        $stats['cpu'] = min(100, ($load[0] / 2.0) * 100);
44
45        // 5-minute average for green bar
46        $stats['cpu_5min'] = min(100, ($load[1] / 2.0) * 100);
47
48        // Store all three load averages for tooltip
49        $stats['load'] = [
50            '1min' => round($load[0], 2),
51            '5min' => round($load[1], 2),
52            '15min' => round($load[2], 2)
53        ];
54    }
55}
56
57// Get memory usage
58if (stristr(PHP_OS, 'linux')) {
59    // Linux: Read from /proc/meminfo
60    $meminfo = file_get_contents('/proc/meminfo');
61    if ($meminfo) {
62        preg_match('/MemTotal:\s+(\d+)/', $meminfo, $total);
63        preg_match('/MemAvailable:\s+(\d+)/', $meminfo, $available);
64
65        if (isset($total[1]) && isset($available[1])) {
66            $totalMem = $total[1];
67            $availableMem = $available[1];
68            $usedMem = $totalMem - $availableMem;
69            $stats['memory'] = ($usedMem / $totalMem) * 100;
70        }
71    }
72} elseif (stristr(PHP_OS, 'darwin') || stristr(PHP_OS, 'bsd')) {
73    // macOS/BSD: Use vm_stat
74    $vm_stat = shell_exec('vm_stat');
75    if ($vm_stat) {
76        preg_match('/Pages free:\s+(\d+)\./', $vm_stat, $free);
77        preg_match('/Pages active:\s+(\d+)\./', $vm_stat, $active);
78        preg_match('/Pages inactive:\s+(\d+)\./', $vm_stat, $inactive);
79        preg_match('/Pages wired down:\s+(\d+)\./', $vm_stat, $wired);
80
81        if (isset($free[1], $active[1], $inactive[1], $wired[1])) {
82            $pageSize = 4096; // bytes
83            $totalPages = $free[1] + $active[1] + $inactive[1] + $wired[1];
84            $usedPages = $active[1] + $inactive[1] + $wired[1];
85
86            if ($totalPages > 0) {
87                $stats['memory'] = ($usedPages / $totalPages) * 100;
88            }
89        }
90    }
91} elseif (stristr(PHP_OS, 'win')) {
92    // Windows: Use wmic
93    $wmic = shell_exec('wmic OS get FreePhysicalMemory,TotalVisibleMemorySize /Value');
94    if ($wmic) {
95        preg_match('/FreePhysicalMemory=(\d+)/', $wmic, $free);
96        preg_match('/TotalVisibleMemorySize=(\d+)/', $wmic, $total);
97
98        if (isset($free[1]) && isset($total[1])) {
99            $freeMem = $free[1];
100            $totalMem = $total[1];
101            $usedMem = $totalMem - $freeMem;
102            $stats['memory'] = ($usedMem / $totalMem) * 100;
103        }
104    }
105}
106
107// Fallback: Use PHP memory if system memory unavailable
108if ($stats['memory'] == 0) {
109    $memLimit = ini_get('memory_limit');
110    if ($memLimit != '-1') {
111        $memLimitBytes = return_bytes($memLimit);
112        $memUsage = memory_get_usage(true);
113        $stats['memory'] = ($memUsage / $memLimitBytes) * 100;
114    }
115}
116
117// Get uptime (Linux/Unix)
118if (file_exists('/proc/uptime')) {
119    $uptime = file_get_contents('/proc/uptime');
120    if ($uptime) {
121        $uptimeSeconds = floatval(explode(' ', $uptime)[0]);
122        $days = floor($uptimeSeconds / 86400);
123        $hours = floor(($uptimeSeconds % 86400) / 3600);
124        $minutes = floor(($uptimeSeconds % 3600) / 60);
125        $stats['uptime'] = sprintf('%dd %dh %dm', $days, $hours, $minutes);
126    }
127} elseif (stristr(PHP_OS, 'win')) {
128    // Windows uptime
129    $wmic = shell_exec('wmic os get lastbootuptime');
130    if ($wmic && preg_match('/(\d{14})/', $wmic, $matches)) {
131        $bootTime = DateTime::createFromFormat('YmdHis', $matches[1]);
132        $now = new DateTime();
133        $diff = $now->diff($bootTime);
134        $stats['uptime'] = sprintf('%dd %dh %dm', $diff->days, $diff->h, $diff->i);
135    }
136}
137
138// Get detailed memory info (Linux)
139if (stristr(PHP_OS, 'linux') && file_exists('/proc/meminfo')) {
140    $meminfo = file_get_contents('/proc/meminfo');
141    if ($meminfo) {
142        preg_match('/MemTotal:\s+(\d+)/', $meminfo, $total);
143        preg_match('/MemAvailable:\s+(\d+)/', $meminfo, $available);
144        preg_match('/MemFree:\s+(\d+)/', $meminfo, $free);
145        preg_match('/Buffers:\s+(\d+)/', $meminfo, $buffers);
146        preg_match('/Cached:\s+(\d+)/', $meminfo, $cached);
147
148        if (isset($total[1])) {
149            $totalMB = round($total[1] / 1024, 1);
150            $availableMB = isset($available[1]) ? round($available[1] / 1024, 1) : 0;
151            $usedMB = round(($total[1] - ($available[1] ?? $free[1] ?? 0)) / 1024, 1);
152            $buffersMB = isset($buffers[1]) ? round($buffers[1] / 1024, 1) : 0;
153            $cachedMB = isset($cached[1]) ? round($cached[1] / 1024, 1) : 0;
154
155            $stats['memory_details'] = [
156                'total' => $totalMB . ' MB',
157                'used' => $usedMB . ' MB',
158                'available' => $availableMB . ' MB',
159                'buffers' => $buffersMB . ' MB',
160                'cached' => $cachedMB . ' MB'
161            ];
162        }
163    }
164}
165
166// Get top 5 processes by CPU (Linux/Unix)
167if (stristr(PHP_OS, 'linux') || stristr(PHP_OS, 'darwin')) {
168    $ps = shell_exec('ps aux --sort=-%cpu | head -6 | tail -5 2>/dev/null');
169    if (!$ps) {
170        // Try BSD/macOS format
171        $ps = shell_exec('ps aux -r | head -6 | tail -5 2>/dev/null');
172    }
173    if ($ps) {
174        $lines = explode("\n", trim($ps));
175        foreach ($lines as $line) {
176            if (empty($line)) continue;
177            $parts = preg_split('/\s+/', $line, 11);
178            if (count($parts) >= 11) {
179                $stats['top_processes'][] = [
180                    'cpu' => $parts[2] . '%',
181                    'mem' => $parts[3] . '%',
182                    'command' => substr($parts[10], 0, 30)
183                ];
184            }
185        }
186    }
187} elseif (stristr(PHP_OS, 'win')) {
188    // Windows top processes
189    $wmic = shell_exec('wmic process get Caption,KernelModeTime /format:csv | findstr /V "^$" | sort /R /+1 | more +1 | findstr /N "^" | findstr "^[1-5]:"');
190    if ($wmic) {
191        $lines = explode("\n", trim($wmic));
192        foreach ($lines as $line) {
193            if (preg_match('/^\d+:(.+),(.+),(\d+)/', $line, $matches)) {
194                $stats['top_processes'][] = [
195                    'command' => substr($matches[2], 0, 30),
196                    'cpu' => '-'
197                ];
198            }
199        }
200    }
201}
202
203echo json_encode($stats);
204
205function return_bytes($val) {
206    $val = trim($val);
207    $last = strtolower($val[strlen($val)-1]);
208    $val = (int)$val;
209    // Intentional fallthrough for unit conversion cascade
210    switch($last) {
211        case 'g':
212            $val *= 1024;
213            // fallthrough intentional
214        case 'm':
215            $val *= 1024;
216            // fallthrough intentional
217        case 'k':
218            $val *= 1024;
219    }
220    return $val;
221}
222