Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.89% covered (success)
98.89%
89 / 90
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
HtmlRenderer
98.89% covered (success)
98.89%
89 / 90
83.33% covered (warning)
83.33%
5 / 6
62
0.00% covered (danger)
0.00%
0 / 1
 render
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 getHtmlHeader
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderServerInfo
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
25
 renderPlayers
97.22% covered (success)
97.22%
35 / 36
0.00% covered (danger)
0.00%
0 / 1
26
 renderRules
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
8
 getHtmlFooter
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
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
13namespace Clansuite\ServerQuery\Util;
14
15use function array_key_exists;
16use function array_keys;
17use function count;
18use function htmlentities;
19use function is_array;
20use function is_numeric;
21use function is_object;
22use function is_scalar;
23use function json_encode;
24use function ucfirst;
25use function usort;
26
27/**
28 * HtmlRenderer for displaying game server query results in HTML format.
29 */
30class 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}