Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
80.00% covered (warning)
80.00%
72 / 90
66.67% covered (warning)
66.67%
6 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
HttpUtils
80.00% covered (warning)
80.00%
72 / 90
66.67% covered (warning)
66.67%
6 / 9
38.69
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
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getEInkRegexes
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 isEInk
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 countRequest
85.71% covered (warning)
85.71%
12 / 14
0.00% covered (danger)
0.00%
0 / 1
5.07
 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
77.78% covered (warning)
77.78%
14 / 18
0.00% covered (danger)
0.00%
0 / 1
6.40
 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 PhpTypeScriptApi\PhpStan\PhpStanUtils;
10use PhpTypeScriptApi\PhpStan\ValidateVisitor;
11use Symfony\Component\HttpFoundation\Request;
12
13class HttpUtils {
14    use WithUtilsTrait;
15
16    /** @return array<string> */
17    public function getBotRegexes(): array {
18        return [
19            '/bingbot/i',
20            '/googlebot/i',
21            '/google/i',
22            '/facebookexternalhit/i',
23            '/applebot/i',
24            '/yandexbot/i',
25            '/ecosia/i',
26            '/phpservermon/i',
27            '/OlzSystemTest\//i',
28            '/bot\//i',
29            '/crawler\//i',
30        ];
31    }
32
33    public function isBot(string $user_agent): bool {
34        foreach ($this->getBotRegexes() as $regex) {
35            if (preg_match($regex, $user_agent)) {
36                return true;
37            }
38        }
39        return false;
40    }
41
42    /** @return array<string> */
43    public function getEInkRegexes(): array {
44        return [
45            '/kindle\//i',
46            '/pocketbook\//i',
47        ];
48    }
49
50    public function isEInk(string $user_agent): bool {
51        foreach ($this->getEInkRegexes() as $regex) {
52            if (preg_match($regex, $user_agent)) {
53                return true;
54            }
55        }
56        return false;
57    }
58
59    /** @param array<string> $get_params */
60    public function countRequest(Request $request, array $get_params = []): void {
61        $user_agent = $this->server()['HTTP_USER_AGENT'] ?? '';
62        if ($this->isBot($user_agent)) {
63            $this->log()->debug("Counter: user agent is bot: {$user_agent}");
64            return;
65        }
66        $path = "{$request->getBasePath()}{$request->getPathInfo()}";
67        $query = [];
68        foreach ($get_params as $key) {
69            $value = $request->query->get($key);
70            if ($value !== null) {
71                $query[] = "{$key}={$value}";
72            }
73        }
74        $pretty_query = empty($query) ? '' : '?'.implode('&', $query);
75        $counter_repo = $this->entityManager()->getRepository(Counter::class);
76        $counter_repo->record("{$path}{$pretty_query}");
77        $this->log()->debug("Counter: Counted {$path}{$pretty_query} (user agent: {$user_agent})");
78    }
79
80    /** @param array<string> $get_params */
81    public function stripParams(Request $request, array $get_params = []): void {
82        $should_strip = false;
83        $query = [];
84        foreach ($request->query->all() as $key => $value) {
85            if (in_array($key, $get_params)) {
86                $should_strip = true;
87            } elseif (is_string($value)) {
88                $query[] = "{$key}={$value}";
89            }
90        }
91        if (!$should_strip) {
92            return;
93        }
94        $path = "{$request->getBasePath()}{$request->getPathInfo()}";
95        $pretty_query = empty($query) ? '' : '?'.implode('&', $query);
96        $this->redirect("{$path}{$pretty_query}", 308);
97    }
98
99    public function dieWithHttpError(int $http_status_code): void {
100        $this->sendHttpResponseCode($http_status_code);
101
102        $out = OlzErrorPage::render([
103            'http_status_code' => $http_status_code,
104        ]);
105
106        $this->sendHttpBody($out);
107        $this->exitExecution();
108    }
109
110    public function redirect(string $redirect_url, int $http_status_code = 301): void {
111        $this->sendHeader("Location: {$redirect_url}");
112        $this->sendHttpResponseCode($http_status_code);
113
114        $out = "";
115        $out .= OlzHeaderWithoutRouting::render([
116            'title' => "Weiterleitung...",
117        ]);
118
119        $enc_redirect_url = json_encode($redirect_url);
120        $out .= <<<ZZZZZZZZZZ
121            <div class='content-full'>
122                <h2>Automatische Weiterleitung...</h2>
123                <p>Falls die automatische Weiterleitung nicht funktionieren sollte, kannst du auch diesen Link anklicken:</p>
124                <p><b><a href='{$redirect_url}' class='linkint' id='redirect-link'>{$redirect_url}</a></b></p>
125                <script type='text/javascript'>
126                    window.setTimeout(function () {
127                        window.location.href = {$enc_redirect_url};
128                    }, 1000);
129                </script>
130            </div>
131            ZZZZZZZZZZ;
132
133        $out .= OlzFooter::render();
134        $this->sendHttpBody($out);
135        $this->exitExecution();
136    }
137
138    /**
139     * @template T of array
140     *
141     * @param class-string<HttpParams<T>>             $params_class
142     * @param ?array<string, ?(string|array<string>)> $get_params
143     * @param array{just_log?: bool}                  $options
144     *
145     * @return T
146     */
147    public function validateGetParams(string $params_class, ?array $get_params = null, array $options = []): array {
148        if ($get_params === null) {
149            $get_params = $this->getParams();
150        }
151        $class_info = new \ReflectionClass($params_class);
152        $utils = new PhpStanUtils();
153        $php_doc_node = $utils->parseDocComment(
154            $class_info->getDocComment(),
155            $class_info->getFileName() ?: null,
156        );
157        $type = $php_doc_node?->getExtendsTagValues()[0]->type->genericTypes[0];
158        $aliases = $utils->getAliases($php_doc_node);
159        if (!$type) {
160            $this->dieWithHttpError(400);
161            throw new \Exception('should already have failed');
162        }
163        $result = ValidateVisitor::validateDeserialize($utils, $get_params, $type, $aliases);
164        if (!$result->isValid() && ($options['just_log'] ?? false) === false) {
165            $this->dieWithHttpError(400);
166            throw new \Exception('should already have failed');
167        }
168        return $result->getValue();
169    }
170
171    // @codeCoverageIgnoreStart
172    // Reason: Mock functions for tests.
173
174    protected function sendHttpResponseCode(int $http_response_code): void {
175        http_response_code($http_response_code);
176    }
177
178    protected function sendHeader(string $http_header_line): void {
179        header($http_header_line);
180    }
181
182    protected function sendHttpBody(string $http_body): void {
183        echo $http_body;
184    }
185
186    protected function exitExecution(): void {
187        exit('');
188    }
189
190    // @codeCoverageIgnoreEnd
191}