Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
46.32% covered (danger)
46.32%
44 / 95
40.00% covered (danger)
40.00%
4 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
HttpUtils
46.32% covered (danger)
46.32%
44 / 95
40.00% covered (danger)
40.00%
4 / 10
212.85
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
 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    /** @return array<string> */
19    public function getBotRegexes(): array {
20        return [
21            '/bingbot/i',
22            '/googlebot/i',
23            '/google/i',
24            '/facebookexternalhit/i',
25            '/applebot/i',
26            '/yandexbot/i',
27            '/ecosia/i',
28            '/phpservermon/i',
29            '/OlzSystemTest\//i',
30            '/bot\//i',
31            '/crawler\//i',
32        ];
33    }
34
35    public function isBot(string $user_agent): bool {
36        foreach ($this->getBotRegexes() as $regex) {
37            if (preg_match($regex, $user_agent)) {
38                return true;
39            }
40        }
41        return false;
42    }
43
44    /** @return array<string> */
45    public function getEInkRegexes(): array {
46        return [
47            '/kindle\//i',
48            '/pocketbook\//i',
49        ];
50    }
51
52    public function isEInk(string $user_agent): bool {
53        foreach ($this->getEInkRegexes() as $regex) {
54            if (preg_match($regex, $user_agent)) {
55                return true;
56            }
57        }
58        return false;
59    }
60
61    /**
62     * @param array<string>               $get_params
63     * @param callable(Request): Response $get_response
64     */
65    #[IgnoreInTrace]
66    public function measure(
67        Request $request,
68        array $get_params,
69        callable $get_response,
70    ): Response {
71        $is_bot = $this->isBot($this->server()['HTTP_USER_AGENT'] ?? '');
72        $normalized_path = $this->getNormalizedPath($request, $get_params);
73        $counter_repo = $this->entityManager()->getRepository(Counter::class);
74        if (!$is_bot) {
75            try {
76                $counter_repo->recordVisit($normalized_path);
77            } catch (\Throwable $th) {
78            }
79        }
80        $started_at = microtime(true);
81        $response = $get_response($request);
82        $duration = microtime(true) - $started_at;
83        try {
84            $counter_repo->recordLatency($normalized_path, $duration * 1000);
85        } catch (\Throwable $th) {
86        }
87        return $response;
88    }
89
90    /** @param array<string> $get_params */
91    public function getNormalizedPath(Request $request, array $get_params = []): string {
92        $path = "{$request->getBasePath()}{$request->getPathInfo()}";
93        $query = [];
94        foreach ($get_params as $key) {
95            $value = $request->query->get($key);
96            if ($value !== null) {
97                $query[] = "{$key}={$value}";
98            }
99        }
100        $pretty_query = empty($query) ? '' : '?'.implode('&', $query);
101        return "{$path}{$pretty_query}";
102    }
103
104    /** @param array<string> $get_params */
105    public function stripParams(Request $request, array $get_params = []): void {
106        $should_strip = false;
107        $query = [];
108        foreach ($request->query->all() as $key => $value) {
109            if (in_array($key, $get_params)) {
110                $should_strip = true;
111            } elseif (is_string($value)) {
112                $query[] = "{$key}={$value}";
113            }
114        }
115        if (!$should_strip) {
116            return;
117        }
118        $path = "{$request->getBasePath()}{$request->getPathInfo()}";
119        $pretty_query = empty($query) ? '' : '?'.implode('&', $query);
120        $this->redirect("{$path}{$pretty_query}", 308);
121    }
122
123    public function dieWithHttpError(int $http_status_code): void {
124        $this->sendHttpResponseCode($http_status_code);
125
126        $out = OlzErrorPage::render([
127            'http_status_code' => $http_status_code,
128        ]);
129
130        $this->sendHttpBody($out);
131        $this->exitExecution();
132    }
133
134    public function redirect(string $redirect_url, int $http_status_code = 301): void {
135        $this->sendHeader("Location: {$redirect_url}");
136        $this->sendHttpResponseCode($http_status_code);
137
138        $out = "";
139        $out .= OlzHeaderWithoutRouting::render([
140            'title' => "Weiterleitung...",
141        ]);
142
143        $enc_redirect_url = json_encode($redirect_url);
144        $out .= <<<ZZZZZZZZZZ
145            <div class='content-full'>
146                <h2>Automatische Weiterleitung...</h2>
147                <p>Falls die automatische Weiterleitung nicht funktionieren sollte, kannst du auch diesen Link anklicken:</p>
148                <p><b><a href='{$redirect_url}' class='linkint' id='redirect-link'>{$redirect_url}</a></b></p>
149                <script type='text/javascript'>
150                    window.setTimeout(function () {
151                        window.location.href = {$enc_redirect_url};
152                    }, 1000);
153                </script>
154            </div>
155            ZZZZZZZZZZ;
156
157        $out .= OlzFooter::render();
158        $this->sendHttpBody($out);
159        $this->exitExecution();
160    }
161
162    /**
163     * @template T of array
164     *
165     * @param class-string<HttpParams<T>>             $params_class
166     * @param ?array<string, ?(string|array<string>)> $get_params
167     * @param array{just_log?: bool}                  $options
168     *
169     * @return T
170     */
171    public function validateGetParams(string $params_class, ?array $get_params = null, array $options = []): array {
172        if ($get_params === null) {
173            $get_params = $this->getParams();
174        }
175        $utils = new PhpStanUtils();
176        $generics = $utils->getSuperGenerics($params_class, HttpParams::class);
177        $type = $generics[0] ?? null;
178        if (!$type) {
179            $this->dieWithHttpError(400);
180            throw new \Exception('should already have failed');
181        }
182        $resolved_type = $utils->resolveType($type, $params_class);
183        if (!$resolved_type instanceof TypeNode) {
184            $this->dieWithHttpError(400);
185            throw new \Exception('should already have failed');
186        }
187        $result = ValidateVisitor::validateDeserialize($utils, $get_params, $resolved_type, []);
188        if (!$result->isValid() && ($options['just_log'] ?? false) === false) {
189            $this->dieWithHttpError(400);
190            throw new \Exception('should already have failed');
191        }
192        return $result->getValue();
193    }
194
195    // @codeCoverageIgnoreStart
196    // Reason: Mock functions for tests.
197
198    protected function sendHttpResponseCode(int $http_response_code): void {
199        http_response_code($http_response_code);
200    }
201
202    protected function sendHeader(string $http_header_line): void {
203        header($http_header_line);
204    }
205
206    protected function sendHttpBody(string $http_body): void {
207        echo $http_body;
208    }
209
210    protected function exitExecution(): void {
211        exit('');
212    }
213
214    // @codeCoverageIgnoreEnd
215}