Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
64.44% covered (warning)
64.44%
116 / 180
90.32% covered (success)
90.32%
28 / 31
CRAP
0.00% covered (danger)
0.00%
0 / 1
Util
64.44% covered (warning)
64.44%
116 / 180
90.32% covered (success)
90.32%
28 / 31
472.45
0.00% covered (danger)
0.00%
0 / 1
 getCookieDomain
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setCookie
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 clearCookie
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
2.00
 clearNumericCookie
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 clearRequiredNumericCookie
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getCookie
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 generateRandomString
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getPercentButAvoid100UntilComplete
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 encrypt
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 decrypt
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 extract
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 extractWithDefault
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getRatio
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getPercent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 indexByID
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 isInGithubCI
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 isInTestEnvironment
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getMyAddress
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 getInternalAddress
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addSqlCondition
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 addSqlOrCondition
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 boolString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getHealthBasedOnLevel
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 query
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 clampOptional
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 strOrNull
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getGraphColor
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getGraphGridColor
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getValueGraphHeight
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 smallScoreTableRowColor
6.15% covered (danger)
6.15%
4 / 65
0.00% covered (danger)
0.00%
0 / 1
933.07
1<?php
2
3class Util
4{
5    public static function getCookieDomain(): ?string
6    {
7        $host = preg_replace('/:\d+$/', '', $_SERVER['HTTP_HOST'] ?? '');
8        return $host;
9    }
10
11    public static function setCookie($name, $value)
12    {
13        setcookie($name, $value, [
14            'expires'  => time() + 365 * 24 * 60 * 60,
15            'path'     => '/',
16            'secure'   => true,
17            'httponly' => false,
18            'samesite' => 'Lax'
19        ]);
20        // Also update $_COOKIE so the value is available in the current request
21        $_COOKIE[$name] = $value;
22    }
23
24    /* @return The value of the cleared cookie */
25    public static function clearCookie(string $name): ?string
26    {
27        $previous = $_COOKIE[$name] ?? null;
28
29        setcookie($name, '',
30            [
31                'expires'  => time() - 3600,
32                'path'     => '/',
33                'secure'   => true,
34                'httponly' => false,
35                'samesite' => 'Lax'
36            ]);
37
38        unset($_COOKIE[$name]);
39        if ($previous == 'deleted')
40            return null;
41        return $previous;
42    }
43
44    /* @return Int value of the cleared cookie, returns null if the cookeie isn't present. throws if it isn't numeric */
45    public static function clearNumericCookie(string $name): ?int
46    {
47        $result = Util::clearCookie($name);
48        if (!$result)
49            return null;
50        if (!is_numeric($result))
51            throw new Exception("Cookie " . $name . " should be numeric but has value '" . strval($result) . "'.");
52        return intval($result);
53    }
54
55    /* @return Int value of the cleared cookie, throws excpetion when the cookie isn't present or isn't numeric */
56    public static function clearRequiredNumericCookie(string $name): int
57    {
58        $result = Util::clearNumericCookie($name);
59        if (is_null($result))
60            throw new Exception("Cookie " . $name . " is expected to be defined, but it isn't");
61        return $result;
62    }
63
64    public static function getCookie(string $name, $default = null)
65    {
66        return isset($_COOKIE[$name]) ? $_COOKIE[$name] : $default;
67    }
68
69    public static function generateRandomString($length = 20)
70    {
71        $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
72        $charactersLength = strlen($characters);
73        $randomString = '';
74        for ($i = 0; $i < $length; $i++)
75            $randomString .= $characters[rand(0, $charactersLength - 1)];
76        return $randomString;
77    }
78
79    public static function getPercentButAvoid100UntilComplete(int $value, int $max): int
80    {
81        assert($value >= 0);
82        assert($max >= 0);
83        assert($value <= $max);
84        $result = (int) round(Util::getRatio($value, $max) * 100);
85        if ($result == 100 && $value < $max)
86            return 99;
87        return $result;
88    }
89
90    private const SECRET_KEY = 'my_simple_secret_keyx';
91    private const SECRET_IV = 'my_simple_secret_ivx';
92    private const ENCRYPT_METHOD = 'AES-256-CBC';
93
94    public static function encrypt(string $str): string
95    {
96        $key = hash('sha256', self::SECRET_KEY);
97        $iv = substr(hash('sha256', self::SECRET_IV), 0, 16);
98        return base64_encode(openssl_encrypt($str, self::ENCRYPT_METHOD, $key, 0, $iv));
99    }
100
101    public static function decrypt(string $str): string
102    {
103        $key = hash('sha256', self::SECRET_KEY);
104        $iv = substr(hash('sha256', self::SECRET_IV), 0, 16);
105        return openssl_decrypt(base64_decode($str), self::ENCRYPT_METHOD, $key, 0, $iv);
106    }
107
108    public static function extract(string $name, array &$inputArray)
109    {
110        $result = $inputArray[$name];
111        unset($inputArray[$name]);
112        return $result;
113    }
114
115    public static function extractWithDefault(string $name, array &$inputArray, $default)
116    {
117        $result = $inputArray[$name];
118        unset($inputArray[$name]);
119        if (isset($result))
120            return $result;
121        return $default;
122    }
123
124    public static function getRatio(float|int $amount, float|int $max): float
125    {
126        if ($max == 0)
127            return 0;
128        return $amount / $max;
129    }
130
131    public static function getPercent(float|int $amount, float|int $max): float
132    {
133        return self::getRatio($amount, $max) * 100;
134    }
135
136    public static function indexByID($array, $prefix1, $prefix2)
137    {
138        $result = [];
139        foreach ($array as $value)
140            $result[$value[$prefix1]['id']] = $value[$prefix1][$prefix2];
141        return $result;
142    }
143
144    public static function isInGithubCI()
145    {
146        if ($testEnvironment = @$_SERVER['TEST_ENVIRONMENT'])
147            return $testEnvironment == 'github-ci';
148        if ($host = @$_SERVER['HTTP_HOST'])
149            return str_contains($host, 'host.docker.internal');
150        return false;
151    }
152
153    public static function isInTestEnvironment(): bool
154    {
155        if (@$_SERVER['DDEV_PRIMARY_URL'] && str_contains($_SERVER['DDEV_PRIMARY_URL'], "tsumego.ddev.site"))
156            return true;
157        return Util::isInGithubCI();
158    }
159
160    public static function getMyAddress()
161    {
162        if (Util::isInGithubCI())
163            return $_SERVER['TEST_APP_URL'];
164        if ($url = @$_SERVER['DDEV_PRIMARY_URL'] && $_SERVER['HTTP_X_FORWARDED_HOST'])
165            return 'https://' . $_SERVER['HTTP_X_FORWARDED_HOST'];
166        return "https://test.tsumego.ddev.site";
167    }
168
169    public static function getInternalAddress()
170    {
171        if (Util::isInGithubCI())
172            return 'https://host.docker.internal:8443';
173        return 'http://localhost';
174    }
175
176    public static function addSqlCondition(&$existingCondition, $condition): void
177    {
178        if (empty($condition))
179            return;
180        if (empty($existingCondition))
181        {
182            $existingCondition = $condition;
183            return;
184        }
185        $existingCondition .= " AND " ;
186        if (str_contains($condition, " OR "))
187            $existingCondition .= '(' . $condition . ')';
188        else
189            $existingCondition .= $condition;
190    }
191
192    public static function addSqlOrCondition(&$existingCondition, $condition): void
193    {
194        if (empty($existingCondition))
195        {
196            $existingCondition = $condition;
197            return;
198        }
199        $existingCondition .= " OR " . $condition;
200    }
201
202    public static function boolString($bool)
203    {
204        return $bool ? 'true' : 'false';
205    }
206
207    public static function getHealthBasedOnLevel(int $level): int
208    {
209        return intdiv($level, 5) + 10;
210    }
211
212    public static function query($sql, $params = [])
213    {
214        /** @phpstan-ignore-next-line */
215        $stmt = ClassRegistry::init('Tsumego')->getDataSource()->getConnection()->prepare($sql);
216        $stmt->execute($params);
217        return $stmt->fetchAll(PDO::FETCH_ASSOC);
218    }
219
220    /**
221     * Execute a non-SELECT SQL statement (INSERT, UPDATE, DELETE) and return the number of affected rows.
222     */
223    public static function execute($sql, $params = []): int
224    {
225        /** @phpstan-ignore-next-line */
226        $stmt = ClassRegistry::init('Tsumego')->getDataSource()->getConnection()->prepare($sql);
227        $stmt->execute($params);
228        return $stmt->rowCount();
229    }
230
231    public static function clampOptional($value, $min, $max)
232    {
233        $result = $value;
234        if (!is_null($min))
235            $result = max($min, $result);
236        if (!is_null($max))
237            $result = min($max, $result);
238        return $result;
239    }
240
241    public static function strOrNull($input): ?string
242    {
243        if (is_null($input))
244            return null;
245        return strval($input);
246    }
247
248    public static function getGraphColor(): string
249    {
250        return Auth::lightMode() == Auth::$LIGHT_MODE ? '#ddd' : '#3e3e3e';
251    }
252
253    public static function getGraphGridColor(): string
254    {
255        return Auth::lightMode() == Auth::$LIGHT_MODE ? '#000' : '#fff';
256    }
257
258    public static function getValueGraphHeight($input)
259    {
260        return 160 + count($input) * 25;
261    }
262
263    public static function smallScoreTableRowColor(int $index): string
264    {
265        if ($index == 0)
266            return '#ffec85';
267        if ($index == 1)
268            return '#939393';
269        if ($index == 2)
270            return '#c28d47';
271        if ($index == 3)
272            return '#85e35d';
273        if ($index == 4)
274            return '#85e35d';
275        if ($index == 5)
276            return '#85e35d';
277        if ($index == 6)
278            return '#85e35d';
279        if ($index == 7)
280            return '#85e35d';
281        if ($index == 8)
282            return '#85e35d';
283        if ($index == 9)
284            return '#85e35d';
285        if ($index == 10)
286            return '#85e35d';
287        if ($index == 11)
288            return '#85e35d';
289        if ($index == 12)
290            return '#85e35d';
291        if ($index == 13)
292            return '#85e35d';
293        if ($index == 14)
294            return '#85e35d';
295        if ($index == 15)
296            return '#85e35d';
297        if ($index == 16)
298            return '#85e35d';
299        if ($index == 17)
300            return '#85e35d';
301        if ($index == 18)
302            return '#85e35d';
303        if ($index == 19)
304            return '#85e35d';
305        if ($index == 20)
306            return '#9cf974';
307        if ($index == 21)
308            return '#9cf974';
309        if ($index == 22)
310            return '#9cf974';
311        if ($index == 23)
312            return '#9cf974';
313        if ($index == 24)
314            return '#9cf974';
315        if ($index == 25)
316            return '#9cf974';
317        if ($index == 26)
318            return '#9cf974';
319        if ($index == 27)
320            return '#9cf974';
321        if ($index == 28)
322            return '#9cf974';
323        if ($index == 29)
324            return '#9cf974';
325        if ($index < 40)
326            return '#b6f998';
327        if ($index < 50)
328            return '#d3f9c2';
329        return '#e8f9e0';
330    }
331}