Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.89% covered (warning)
86.89%
53 / 61
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
GetLogsEndpoint
86.89% covered (warning)
86.89%
53 / 61
66.67% covered (warning)
66.67%
2 / 3
13.38
0.00% covered (danger)
0.00%
0 / 1
 handle
77.78% covered (warning)
77.78%
28 / 36
0.00% covered (danger)
0.00%
0 / 1
7.54
 serializePageToken
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 deserializePageToken
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3namespace Olz\Apps\Logs\Endpoints;
4
5use Olz\Api\OlzTypedEndpoint;
6use Olz\Apps\Logs\Utils\GzLogFile;
7use Olz\Apps\Logs\Utils\HybridLogFile;
8use Olz\Apps\Logs\Utils\LineLocation;
9use Olz\Apps\Logs\Utils\LogsDefinitions;
10use Olz\Apps\Logs\Utils\PlainLogFile;
11use PhpTypeScriptApi\HttpError;
12use PhpTypeScriptApi\PhpStan\IsoDateTime;
13
14/**
15 * @phpstan-type OlzLogLevel 'debug'|'info'|'notice'|'warning'|'error'|'critical'|'alert'|'emergency'
16 * @phpstan-type OlzLogsQuery array{
17 *   channel: string,
18 *   targetDate?: ?IsoDateTime,
19 *   firstDate?: ?IsoDateTime,
20 *   lastDate?: ?IsoDateTime,
21 *   minLogLevel?: ?OlzLogLevel,
22 *   textSearch?: ?string,
23 *   pageToken?: ?string,
24 * }
25 *
26 * @extends OlzTypedEndpoint<
27 *   array{query: OlzLogsQuery},
28 *   array{content: array<string>, pagination: array{previous: ?string, next: ?string}},
29 * >
30 */
31class GetLogsEndpoint extends OlzTypedEndpoint {
32    protected function handle(mixed $input): mixed {
33        if (!$this->authUtils()->hasPermission('all')) {
34            throw new HttpError(403, "Kein Zugriff!");
35        }
36
37        $user = $this->authUtils()->getCurrentUser();
38        $this->log()->info("Logs access by {$user?->getUsername()}.");
39
40        $channel = null;
41        foreach (LogsDefinitions::getLogsChannels() as $current_channel) {
42            if ($current_channel::getId() === $input['query']['channel']) {
43                $channel = new $current_channel();
44            }
45        }
46        if (!$channel) {
47            throw new HttpError(404, "Channel not found");
48        }
49        $channel->setEnvUtils($this->envUtils());
50        $channel->setLog($this->log());
51
52        $query = $input['query'];
53        $page_token = $query['pageToken'] ?? null;
54        $date_time = $query['targetDate'] ?? null;
55        $result = null;
56        if ($page_token) {
57            $deserialized = $this->deserializePageToken($page_token);
58            $result = $channel->continueReading(
59                $deserialized['lineLocation'],
60                $deserialized['mode'],
61                $query,
62            );
63        } elseif ($date_time) {
64            $result = $channel->readAroundDateTime(
65                new \DateTime($date_time),
66                $query,
67            );
68        } else {
69            throw new HttpError(400, "Need to provide targetDate or pageToken");
70        }
71
72        return [
73            'content' => $result->lines,
74            'pagination' => [
75                // TODO: Encrypt!
76                'previous' => $this->serializePageToken($result->previous, 'previous'),
77                'next' => $this->serializePageToken($result->next, 'next'),
78            ],
79        ];
80    }
81
82    protected function serializePageToken(
83        ?LineLocation $line_location,
84        ?string $mode,
85    ): ?string {
86        if (!$line_location) {
87            return null;
88        }
89        return json_encode([
90            'logFile' => $line_location->logFile->serialize(),
91            'lineNumber' => $line_location->lineNumber,
92            'comparison' => $line_location->comparison,
93            'mode' => $mode,
94        ]) ?: null;
95    }
96
97    /** @return array{lineLocation: LineLocation, mode: string} */
98    protected function deserializePageToken(
99        string $serialized,
100    ): array {
101        $data = json_decode($serialized, true);
102        $log_file_classes = [
103            HybridLogFile::class,
104            GzLogFile::class,
105            PlainLogFile::class,
106        ];
107        $log_file = null;
108        foreach ($log_file_classes as $log_file_class) {
109            if (!$log_file) {
110                $log_file = $log_file_class::deserialize($data['logFile']);
111            }
112        }
113        $this->generalUtils()->checkNotNull($log_file, "No log file: {$data['logFile']}");
114        $line_location = new LineLocation($log_file, $data['lineNumber'], $data['comparison']);
115        $mode = $data['mode'];
116        return [
117            'lineLocation' => $line_location,
118            'mode' => $mode,
119        ];
120    }
121}