1: <?php declare(strict_types=1);
2:
3: /**
4: * Clansuite Server Query
5: *
6: * SPDX-FileCopyrightText: 2003-2025 Jens A. Koch
7: * SPDX-License-Identifier: MIT
8: *
9: * For the full copyright and license information, please view
10: * the LICENSE file that was distributed with this source code.
11: */
12:
13: namespace Clansuite\ServerQuery\Util;
14:
15: use function array_key_exists;
16: use function array_keys;
17: use function count;
18: use function htmlentities;
19: use function is_array;
20: use function is_numeric;
21: use function is_object;
22: use function is_scalar;
23: use function json_encode;
24: use function ucfirst;
25: use function usort;
26:
27: /**
28: * HtmlRenderer for displaying game server query results in HTML format.
29: */
30: class HtmlRenderer
31: {
32: /**
33: * Render server data as HTML tables.
34: *
35: * @param array $data The decoded JSON data from toJson()
36: * @param array<mixed> $data
37: *
38: * @return string HTML representation of the server data
39: */
40: public function render(array $data): string
41: {
42: $html = $this->getHtmlHeader();
43: $html .= $this->renderServerInfo($data);
44: $html .= '<div class="tables-container">';
45: $html .= $this->renderPlayers($data);
46: $html .= $this->renderRules($data);
47: $html .= '</div>';
48: $html .= $this->getHtmlFooter();
49:
50: return $html;
51: }
52:
53: /**
54: * Get HTML header with basic styling.
55: */
56: private function getHtmlHeader(): string
57: {
58: return '<!DOCTYPE html>
59: <html lang="en">
60: <head>
61: <meta charset="UTF-8">
62: <meta name="viewport" content="width=device-width, initial-scale=1.0">
63: <title>Game Server Query Results</title>
64: <style>
65: body { font-family: Arial, sans-serif; margin: 20px; background-color: #f5f5f5; }
66: .container { max-width: 1400px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
67: h1, h2 { color: #333; border-bottom: 2px solid #007cba; padding-bottom: 5px; }
68: .tables-container { display: flex; gap: 20px; margin-bottom: 20px; }
69: .table-container { flex: 1; }
70: .table-container table { width: 100%; border-collapse: collapse; background: white; }
71: .table-container th, .table-container td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
72: .table-container th { background-color: #007cba; color: white; font-weight: bold; }
73: .table-container tr:nth-child(even) { background-color: #f9f9f9; }
74: .table-container tr:hover { background-color: #f1f1f1; }
75: .status-online { color: #28a745; font-weight: bold; }
76: .status-offline { color: #dc3545; font-weight: bold; }
77: .server-info { background: #e3f2fd; padding: 15px; border-radius: 5px; margin-bottom: 20px; }
78: .server-info strong { display: inline-block; min-width: 120px; }
79: @media (max-width: 768px) {
80: .tables-container { flex-direction: column; }
81: }
82: </style>
83: </head>
84: <body>
85: <div class="container">
86: <h1>Game Server Query Results</h1>';
87: }
88:
89: /**
90: * Render server basic information.
91: *
92: * @param array<mixed> $data
93: */
94: private function renderServerInfo(array $data): string
95: {
96: $online = isset($data['online']) ? (bool) $data['online'] : false;
97:
98: $address = isset($data['address']) && is_scalar($data['address']) ? (string) $data['address'] : '';
99: $queryport = isset($data['queryport']) && is_scalar($data['queryport']) ? (string) $data['queryport'] : '';
100: $servertitle = isset($data['servertitle']) && is_scalar($data['servertitle']) ? (string) $data['servertitle'] : '';
101: $gamename = isset($data['gamename']) && is_scalar($data['gamename']) ? (string) $data['gamename'] : '';
102: $gameversion = isset($data['gameversion']) && is_scalar($data['gameversion']) ? (string) $data['gameversion'] : '';
103: $mapname = isset($data['mapname']) && is_scalar($data['mapname']) ? (string) $data['mapname'] : '';
104: $gametype = isset($data['gametype']) && is_scalar($data['gametype']) ? (string) $data['gametype'] : '';
105: $numplayers = isset($data['numplayers']) && is_numeric($data['numplayers']) ? (int) $data['numplayers'] : 0;
106: $maxplayers = isset($data['maxplayers']) && is_numeric($data['maxplayers']) ? (int) $data['maxplayers'] : 0;
107: $password = isset($data['password']) && is_scalar($data['password']) ? (string) $data['password'] : '0';
108:
109: $statusClass = $online ? 'status-online' : 'status-offline';
110: $statusText = $online ? 'Online' : 'Offline';
111:
112: $html = '<div class="server-info">';
113: $html .= '<h2>Server Information</h2>';
114: $html .= '<p><strong>Address:</strong> ' . htmlentities($address) . ':' . htmlentities($queryport) . '</p>';
115: $html .= '<p><strong>Status:</strong> <span class="' . $statusClass . '">' . $statusText . '</span></p>';
116: $html .= '<p><strong>Server Name:</strong> ' . htmlentities($servertitle) . '</p>';
117: $html .= '<p><strong>Game:</strong> ' . htmlentities($gamename) . '</p>';
118: $html .= '<p><strong>Version:</strong> ' . htmlentities($gameversion) . '</p>';
119: $html .= '<p><strong>Map:</strong> ' . htmlentities($mapname) . '</p>';
120: $html .= '<p><strong>Gametype:</strong> ' . htmlentities($gametype) . '</p>';
121: $html .= '<p><strong>Players:</strong> ' . $numplayers . '/' . $maxplayers . '</p>';
122: $html .= '<p><strong>Password Protected:</strong> ' . ($password === '0' ? 'No' : 'Yes') . '</p>';
123: $html .= '</div>';
124:
125: return $html;
126: }
127:
128: /**
129: * Render players table.
130: *
131: * @param array<mixed> $data
132: */
133: private function renderPlayers(array $data): string
134: {
135: if (!isset($data['players']) || !is_array($data['players']) || count($data['players']) === 0) {
136: return '<div class="table-container"><h2>Players</h2><p>No players currently online.</p></div>';
137: }
138:
139: // Sort players by score (frags) in descending order
140: $players = $data['players'];
141: usort($players, static function (mixed $a, mixed $b): int
142: {
143: $scoreA = is_array($a) && isset($a['score']) && is_numeric($a['score']) ? (int) $a['score'] : 0;
144: $scoreB = is_array($b) && isset($b['score']) && is_numeric($b['score']) ? (int) $b['score'] : 0;
145:
146: return $scoreB <=> $scoreA; // Descending order
147: });
148:
149: $html = '<div class="table-container">';
150: $html .= '<h2>Players (' . count($players) . ')</h2>';
151: $html .= '<table>';
152: $html .= '<thead><tr>';
153:
154: // Determine available columns from playerkeys
155: $columns = [];
156:
157: if (isset($data['playerkeys']) && is_array($data['playerkeys']) && count($data['playerkeys']) > 0) {
158: foreach ($data['playerkeys'] as $key => $available) {
159: if ((bool) $available) {
160: $columns[] = (string) $key;
161: }
162: }
163: } else {
164: // Fallback: check first player for available keys
165: if (isset($players[0]) && is_array($players[0]) && count($players[0]) > 0) {
166: $columns = array_keys($players[0]);
167: }
168: }
169:
170: foreach ($columns as $column) {
171: $colStr = (string) $column;
172: $html .= '<th>' . ucfirst(htmlentities($colStr)) . '</th>';
173: }
174: $html .= '</tr></thead><tbody>';
175:
176: foreach ($players as $player) {
177: if (!is_array($player)) {
178: continue;
179: }
180: $html .= '<tr>';
181:
182: foreach ($columns as $column) {
183: $value = array_key_exists($column, $player) ? $player[$column] : '';
184: $cell = is_scalar($value) ? (string) $value : (is_object($value) || is_array($value) ? (string) json_encode($value) : '');
185: $html .= '<td>' . htmlentities($cell) . '</td>';
186: }
187: $html .= '</tr>';
188: }
189:
190: $html .= '</tbody></table>';
191: $html .= '</div>';
192:
193: return $html;
194: }
195:
196: /**
197: * Render server rules table.
198: *
199: * @param array<mixed> $data
200: */
201: private function renderRules(array $data): string
202: {
203: if (!isset($data['rules']) || count($data['rules']) === 0) {
204: return '<div class="table-container"><h2>Server Rules</h2><p>No server rules available.</p></div>';
205: }
206:
207: $html = '<div class="table-container">';
208: $html .= '<h2>Server Rules</h2>';
209: $html .= '<table>';
210: $html .= '<thead><tr><th>Rule</th><th>Value</th></tr></thead><tbody>';
211:
212: foreach ($data['rules'] as $key => $value) {
213: $html .= '<tr>';
214: $keyStr = (string) $key;
215: $valueStr = is_scalar($value) ? (string) $value : (is_object($value) || is_array($value) ? (json_encode($value) !== false ? json_encode($value) : '') : '');
216: $html .= '<td>' . htmlentities($keyStr) . '</td>';
217: $html .= '<td>' . htmlentities($valueStr) . '</td>';
218: $html .= '</tr>';
219: }
220:
221: $html .= '</tbody></table>';
222: $html .= '</div>';
223:
224: return $html;
225: }
226:
227: /**
228: * Get HTML footer.
229: */
230: private function getHtmlFooter(): string
231: {
232: return ' </div>
233: </body>
234: </html>';
235: }
236: }
237: