Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
36.67% covered (danger)
36.67%
44 / 120
33.33% covered (danger)
33.33%
4 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
HttpUtils
36.67% covered (danger)
36.67%
44 / 120
33.33% covered (danger)
33.33%
4 / 12
425.39
0.00% covered (danger)
0.00%
0 / 1
 getBotRegexes
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 isBot
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 getEInkRegexes
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 isEInk
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 measure
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 getNormalizedPath
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 stripParams
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 dieWithHttpError
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 redirect
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
1
 validateGetParams
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
42
 curlInit
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
2
 curlExec
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 sendHttpResponseCode
n/a
0 / 0
n/a
0 / 0
1
 sendHeader
n/a
0 / 0
n/a
0 / 0
1
 sendHttpBody
n/a
0 / 0
n/a
0 / 0
1
 exitExecution
n/a
0 / 0
n/a
0 / 0
1
1<?php
2
3namespace Olz\Utils;
4
5use Olz\Components\Error\OlzErrorPage\OlzErrorPage;
6use Olz\Components\Page\OlzFooter\OlzFooter;
7use Olz\Components\Page\OlzHeaderWithoutRouting\OlzHeaderWithoutRouting;
8use Olz\Entity\Counter;
9use PHPStan\PhpDocParser\Ast\Type\TypeNode;
10use PhpTypeScriptApi\PhpStan\PhpStanUtils;
11use PhpTypeScriptApi\PhpStan\ValidateVisitor;
12use Symfony\Component\HttpFoundation\Request;
13use Symfony\Component\HttpFoundation\Response;
14
15class HttpUtils {
16    use WithUtilsTrait;
17
18    /** @var non-empty-string */
19    protected static string $user_agent_string = "Mozilla/5.0 (compatible; olz-website/1.0; +https://github.com/olzimmerberg/olz-website/blob/main/src/Utils/HttpUtils.php)";
20
21    /** @return array<string> */
22    public function getBotRegexes(): array {
23        return [
24            '/bingbot/i',
25            '/googlebot/i',
26            '/google/i',
27            '/facebookexternalhit/i',
28            '/applebot/i',
29            '/yandexbot/i',
30            '/ecosia/i',
31            '/phpservermon/i',
32            '/OlzSystemTest\//i',
33            '/bot\//i',
34            '/crawler\//i',
35        ];
36    }
37
38    public function isBot(string $user_agent): bool {
39        foreach ($this->getBotRegexes() as $regex) {
40            if (preg_match($regex, $user_agent)) {
41                return true;
42            }
43        }
44        return false;
45    }
46
47    /** @return array<string> */
48    public function getEInkRegexes(): array {
49        return [
50            '/kindle\//i',
51            '/pocketbook\//i',
52        ];
53    }
54
55    public function isEInk(string $user_agent): bool {
56        foreach ($this->getEInkRegexes() as $regex) {
57            if (preg_match($regex, $user_agent)) {
58                return true;
59            }
60        }
61        return false;
62    }
63
64    /**
65     * @param array<string>               $get_params
66     * @param callable(Request): Response $get_response
67     */
68    #[IgnoreInTrace]
69    public function measure(
70        Request $request,
71        array $get_params,
72        callable $get_response,
73    ): Response {
74        $is_bot = $this->isBot($this->server()['HTTP_USER_AGENT'] ?? '');
75        $normalized_path = $this->getNormalizedPath($request, $get_params);
76        $counter_repo = $this->entityManager()->getRepository(Counter::class);
77        if (!$is_bot) {
78            try {
79                $counter_repo->recordVisit($normalized_path);
80            } catch (\Throwable $th) {
81            }
82        }
83        $started_at = microtime(true);
84        $response = $get_response($request);
85        $duration = microtime(true) - $started_at;
86        try {
87            $counter_repo->recordLatency($normalized_path, $duration * 1000);
88        } catch (\Throwable $th) {
89        }
90        return $response;
91    }
92
93    /** @param array<string> $get_params */
94    public function getNormalizedPath(Request $request, array $get_params = []): string {
95        $path = "{$request->getBasePath()}{$request->getPathInfo()}";
96        $query = [];
97        foreach ($get_params as $key) {
98            $value = $request->query->get($key);
99            if ($value !== null) {
100                $query[] = "{$key}={$value}";
101            }
102        }
103        $pretty_query = empty($query) ? '' : '?'.implode('&', $query);
104        return "{$path}{$pretty_query}";
105    }
106
107    /** @param array<string> $get_params */
108    public function stripParams(Request $request, array $get_params = []): void {
109        $should_strip = false;
110        $query = [];
111        foreach ($request->query->all() as $key => $value) {
112            if (in_array($key, $get_params)) {
113                $should_strip = true;
114            } elseif (is_string($value)) {
115                $query[] = "{$key}={$value}";
116            }
117        }
118        if (!$should_strip) {
119            return;
120        }
121        $path = "{$request->getBasePath()}{$request->getPathInfo()}";
122        $pretty_query = empty($query) ? '' : '?'.implode('&', $query);
123        $this->redirect("{$path}{$pretty_query}", 308);
124    }
125
126    public function dieWithHttpError(int $http_status_code): void {
127        $this->sendHttpResponseCode($http_status_code);
128
129        $out = OlzErrorPage::render([
130            'http_status_code' => $http_status_code,
131        ]);
132
133        $this->sendHttpBody($out);
134        $this->exitExecution();
135    }
136
137    public function redirect(string $redirect_url, int $http_status_code = 301): void {
138        $this->sendHeader("Location: {$redirect_url}");
139        $this->sendHttpResponseCode($http_status_code);
140
141        $out = "";
142        $out .= OlzHeaderWithoutRouting::render([
143            'title' => "Weiterleitung...",
144        ]);
145
146        $enc_redirect_url = json_encode($redirect_url);
147        $out .= <<<ZZZZZZZZZZ
148            <div class='content-full'>
149                <h2>Automatische Weiterleitung...</h2>
150                <p>Falls die automatische Weiterleitung nicht funktionieren sollte, kannst du auch diesen Link anklicken:</p>
151                <p><b><a href='{$redirect_url}' class='linkint' id='redirect-link'>{$redirect_url}</a></b></p>
152                <script type='text/javascript'>
153                    window.setTimeout(function () {
154                        window.location.href = {$enc_redirect_url};
155                    }, 1000);
156                </script>
157            </div>
158            ZZZZZZZZZZ;
159
160        $out .= OlzFooter::render();
161        $this->sendHttpBody($out);
162        $this->exitExecution();
163    }
164
165    /**
166     * @template T of array
167     *
168     * @param class-string<HttpParams<T>>             $params_class
169     * @param ?array<string, ?(string|array<string>)> $get_params
170     * @param array{just_log?: bool}                  $options
171     *
172     * @return T
173     */
174    public function validateGetParams(string $params_class, ?array $get_params = null, array $options = []): array {
175        if ($get_params === null) {
176            $get_params = $this->getParams();
177        }
178        $utils = new PhpStanUtils();
179        $generics = $utils->getSuperGenerics($params_class, HttpParams::class);
180        $type = $generics[0] ?? null;
181        if (!$type) {
182            $this->dieWithHttpError(400);
183            throw new \Exception('should already have failed');
184        }
185        $resolved_type = $utils->resolveType($type, $params_class);
186        if (!$resolved_type instanceof TypeNode) {
187            $this->dieWithHttpError(400);
188            throw new \Exception('should already have failed');
189        }
190        $result = ValidateVisitor::validateDeserialize($utils, $get_params, $resolved_type, []);
191        if (!$result->isValid() && ($options['just_log'] ?? false) === false) {
192            $this->dieWithHttpError(400);
193            throw new \Exception('should already have failed');
194        }
195        return $result->getValue();
196    }
197
198    /**
199     * @param non-empty-string $url
200     * @param array{
201     *   headers?: array<string>,
202     *   userAgent?: non-empty-string,
203     *   connectTimeout?: int,
204     *   timeout?: int,
205     * } $options
206     */
207    public function curlInit(string $url, array $options = []): \CurlHandle {
208        $ch = curl_init();
209
210        $headers = [
211            'Cache-Control: no-cache, no-store, must-revalidate',
212            'Pragma: no-cache',
213            'Expires: 0',
214            ...($options['headers'] ?? []),
215        ];
216        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
217        curl_setopt($ch, CURLOPT_URL, $url);
218        curl_setopt($ch, CURLOPT_USERAGENT, $options['userAgent'] ?? self::$user_agent_string);
219        curl_setopt($ch, CURLOPT_FRESH_CONNECT, true);
220        curl_setopt($ch, CURLOPT_FORBID_REUSE, true);
221        // @phpstan-ignore-next-line argument.type
222        curl_setopt($ch, CURLOPT_PROXY, null);
223        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
224        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
225        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $options['connectTimeout'] ?? 10);
226        curl_setopt($ch, CURLOPT_TIMEOUT, $options['timeout'] ?? 20);
227        return $ch;
228    }
229
230    public function curlExec(\CurlHandle $ch): string {
231        $result = curl_exec($ch);
232        $errno = curl_errno($ch);
233        $error = curl_error($ch);
234        if ($errno || !is_string($result)) {
235            $url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
236            throw new \Exception("Error fetching {$url}{$error} ({$errno})");
237        }
238        return $result ?: '';
239    }
240
241    // @codeCoverageIgnoreStart
242    // Reason: Mock functions for tests.
243
244    protected function sendHttpResponseCode(int $http_response_code): void {
245        http_response_code($http_response_code);
246    }
247
248    protected function sendHeader(string $http_header_line): void {
249        header($http_header_line);
250    }
251
252    protected function sendHttpBody(string $http_body): void {
253        echo $http_body;
254    }
255
256    protected function exitExecution(): void {
257        exit('');
258    }
259
260    // @codeCoverageIgnoreEnd
261}