Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
85.11% covered (warning)
85.11%
40 / 47
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
GeoIpDatabaseStatusService
85.11% covered (warning)
85.11%
40 / 47
60.00% covered (warning)
60.00%
3 / 5
13.56
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getStatus
88.89% covered (warning)
88.89%
32 / 36
0.00% covered (danger)
0.00%
0 / 1
7.07
 isReady
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isSupportedDatabaseType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 readModifiedAt
62.50% covered (warning)
62.50%
5 / 8
0.00% covered (danger)
0.00%
0 / 1
3.47
1<?php
2
3declare(strict_types=1);
4/**
5 * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
6 * SPDX-License-Identifier: AGPL-3.0-or-later
7 */
8
9namespace OCA\Libresign\Service\GeoIp;
10
11use OCA\Libresign\Enum\GeoIpDatabaseStatus;
12use OCA\Libresign\Vendor\GeoIp2\Database\Reader;
13use OCA\Libresign\Vendor\MaxMind\Db\Reader\InvalidDatabaseException;
14use Psr\Log\LoggerInterface;
15
16/**
17 * @psalm-type GeoIpDatabaseStatusPayload = array{
18 *     path: string|null,
19 *     status: string,
20 *     databaseType?: string,
21 *     buildEpoch?: int,
22 *     modifiedAt?: string,
23 * }
24 */
25class GeoIpDatabaseStatusService {
26    /** @var list<string> */
27    public const SUPPORTED_DATABASE_TYPES = [
28        'GeoIP2-City',
29        'GeoLite2-City',
30    ];
31
32    public function __construct(
33        private GeoIpConfigService $geoIpConfigService,
34        private LoggerInterface $logger,
35    ) {
36    }
37
38    /**
39     * @return GeoIpDatabaseStatusPayload
40     */
41    public function getStatus(): array {
42        $path = $this->geoIpConfigService->getDatabasePath();
43        if ($path === null) {
44            return [
45                'path' => null,
46                'status' => GeoIpDatabaseStatus::NOT_CONFIGURED->value,
47            ];
48        }
49
50        $payload = [
51            'path' => $path,
52            'status' => GeoIpDatabaseStatus::NOT_CONFIGURED->value,
53        ];
54
55        if (!file_exists($path)) {
56            $payload['status'] = GeoIpDatabaseStatus::NOT_FOUND->value;
57            return $payload;
58        }
59
60        if (!is_readable($path)) {
61            $payload['status'] = GeoIpDatabaseStatus::NOT_READABLE->value;
62            return $payload;
63        }
64
65        $modifiedAt = $this->readModifiedAt($path);
66        if ($modifiedAt !== null) {
67            $payload['modifiedAt'] = $modifiedAt;
68        }
69
70        try {
71            $reader = new Reader($path);
72            try {
73                $metadata = $reader->metadata();
74                $databaseType = $metadata->databaseType;
75                $payload['databaseType'] = $databaseType;
76                $payload['buildEpoch'] = $metadata->buildEpoch;
77
78                if (!$this->isSupportedDatabaseType($databaseType)) {
79                    $payload['status'] = GeoIpDatabaseStatus::UNSUPPORTED_DATABASE->value;
80                    return $payload;
81                }
82
83                $payload['status'] = GeoIpDatabaseStatus::READY->value;
84                return $payload;
85            } finally {
86                $reader->close();
87            }
88        } catch (InvalidDatabaseException|\Throwable $exception) {
89            $this->logger->warning('GeoIP database validation failed', [
90                'exception' => $exception::class,
91            ]);
92            $payload['status'] = GeoIpDatabaseStatus::INVALID_DATABASE->value;
93            return $payload;
94        }
95    }
96
97    public function isReady(): bool {
98        return ($this->getStatus()['status'] ?? null) === GeoIpDatabaseStatus::READY->value;
99    }
100
101    public function isSupportedDatabaseType(string $databaseType): bool {
102        return in_array($databaseType, self::SUPPORTED_DATABASE_TYPES, true);
103    }
104
105    private function readModifiedAt(string $path): ?string {
106        $mtime = @filemtime($path);
107        if ($mtime === false) {
108            return null;
109        }
110
111        try {
112            return (new \DateTimeImmutable('@' . $mtime))
113                ->setTimezone(new \DateTimeZone('UTC'))
114                ->format(\DateTimeInterface::ATOM);
115        } catch (\Throwable) {
116            return null;
117        }
118    }
119}