Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
80.90% covered (warning)
80.90%
72 / 89
50.00% covered (danger)
50.00%
3 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
CaptchaService
80.90% covered (warning)
80.90%
72 / 89
50.00% covered (danger)
50.00%
3 / 6
19.01
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 ensureValid
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
2.50
 getCaptchaDetails
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 generateToken
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 createChallenge
83.33% covered (warning)
83.33%
20 / 24
0.00% covered (danger)
0.00%
0 / 1
5.12
 verifySolution
71.43% covered (warning)
71.43%
30 / 42
0.00% covered (danger)
0.00%
0 / 1
8.14
1<?php
2
3declare(strict_types=1);
4
5namespace BO\Zmscitizenapi\Services\Captcha;
6
7use BO\Zmscitizenapi\Helper\ClientIpHelper;
8use BO\Zmscitizenapi\Models\CaptchaInterface;
9use BO\Zmscitizenapi\Services\Captcha\CaptchaService;
10use BO\Zmsentities\Schema\Entity;
11use Firebase\JWT\JWT;
12use GuzzleHttp\Client;
13use GuzzleHttp\Exception\RequestException;
14
15class CaptchaService extends Entity implements CaptchaInterface
16{
17    public static $schema = "citizenapi/captcha/altchaCaptcha.json";
18/** @var string */
19    public string $service;
20/** @var string */
21    private string $siteKey;
22/** @var string */
23    private string $siteSecret;
24/** @var string */
25    private string $tokenSecret;
26/** @var int */
27    private int $tokenExpirationSeconds;
28/** @var string */
29    public string $challengeUrl;
30/** @var string */
31    public string $verifyUrl;
32/** @var Client */
33    protected Client $httpClient;
34/**
35     * Constructor.
36     */
37    public function __construct()
38    {
39        $this->service = 'CaptchaService';
40        $this->siteKey = \App::$ALTCHA_CAPTCHA_SITE_KEY;
41        $this->siteSecret = \App::$ALTCHA_CAPTCHA_SITE_SECRET;
42        $this->tokenSecret = \App::$CAPTCHA_TOKEN_SECRET;
43        $this->tokenExpirationSeconds = \App::$CAPTCHA_TOKEN_TTL;
44        $this->challengeUrl = \App::$ALTCHA_CAPTCHA_ENDPOINT_CHALLENGE;
45        $this->verifyUrl = \App::$ALTCHA_CAPTCHA_ENDPOINT_VERIFY;
46        $this->httpClient = new Client(['verify' => false]);
47        $this->ensureValid();
48    }
49
50    private function ensureValid()
51    {
52        if (!$this->testValid()) {
53            throw new \InvalidArgumentException("The provided data is invalid according to the schema.");
54        }
55    }
56
57    /**
58     * Gibt die Captcha-Konfigurationsdetails zurück.
59     *
60     * @return array
61     */
62    public function getCaptchaDetails(): array
63    {
64        return [
65            'siteKey' => $this->siteKey,
66            'captchaChallenge' => $this->challengeUrl,
67            'captchaVerify' => $this->verifyUrl,
68            'captchaEnabled' => \App::$CAPTCHA_ENABLED
69        ];
70    }
71
72    /**
73     * Generiert einen JWT für die Captcha-Validierung.
74     *
75     * @return string
76     */
77    public function generateToken(): string
78    {
79        $payload = [
80            'ip' => ClientIpHelper::getClientIp(),
81            'iat' => time(),
82            'exp' => time() + $this->tokenExpirationSeconds,
83        ];
84
85        return JWT::encode($payload, $this->tokenSecret, 'HS256');
86    }
87
88    /**
89     * Fordert eine neue Captcha-Challenge an.
90     *
91     * @return array
92     * @throws \Exception
93     */
94    public function createChallenge(): array
95    {
96        try {
97            $response = $this->httpClient->post($this->challengeUrl, [
98                'json' => [
99                    'siteKey' => $this->siteKey,
100                    'siteSecret' => $this->siteSecret,
101                    'clientAddress' => ClientIpHelper::getClientIp(),
102                ]
103            ]);
104
105            $responseData = json_decode((string) $response->getBody(), true);
106
107            if (json_last_error() !== JSON_ERROR_NONE) {
108                throw new \Exception('Fehler beim Dekodieren der JSON-Antwort');
109            }
110
111            $challenge = $responseData['challenge'] ?? null;
112
113            if ($challenge === null) {
114                throw new \Exception('Challenge-Daten fehlen in der Antwort');
115            }
116
117            return $challenge;
118        } catch (RequestException $e) {
119            return [
120                'meta' => ['success' => false, 'error' => 'Request-Fehler: ' . $e->getMessage()],
121                'data' => null,
122            ];
123        } catch (\Throwable $e) {
124            return [
125                'meta' => ['success' => false, 'error' => $e->getMessage()],
126                'data' => null,
127            ];
128        }
129    }
130
131    /**
132     * Überprüft die Captcha-Lösung.
133     *
134     * @param string $payload
135     * @return mixed
136     * @throws \Exception
137     */
138    public function verifySolution(?string $payload): array
139    {
140        if (!$payload) {
141            return [
142                'meta' => ['success' => false, 'error' => 'Keine Payload übergeben'],
143                'data' => null,
144            ];
145        }
146
147        $decodedJson = base64_decode(strtr($payload, '-_', '+/'));
148        if (!$decodedJson) {
149            return [
150                'meta' => ['success' => false, 'error' => 'Payload konnte nicht dekodiert werden'],
151                'data' => null,
152            ];
153        }
154
155        $decodedPayload = json_decode($decodedJson, true);
156        if (json_last_error() !== JSON_ERROR_NONE) {
157            return [
158                'meta' => ['success' => false, 'error' => 'Ungültiges JSON in Payload'],
159                'data' => null,
160            ];
161        }
162
163        try {
164            $response = $this->httpClient->post($this->verifyUrl, [
165                'json' => [
166                    'siteKey' => $this->siteKey,
167                    'siteSecret' => $this->siteSecret,
168                    'payload' => $decodedPayload,
169                ]
170            ]);
171
172            $responseData = json_decode((string) $response->getBody(), true);
173
174            if (json_last_error() !== JSON_ERROR_NONE) {
175                throw new \Exception('Antwort vom Captcha-Service ist kein gültiges JSON');
176            }
177
178            return [
179                'meta' => ['success' => true],
180                'data' => $responseData,
181                'token' => $this->generateToken(),
182            ];
183        } catch (RequestException $e) {
184            return [
185                'meta' => ['success' => false, 'error' => 'Request-Fehler: ' . $e->getMessage()],
186                'data' => null,
187            ];
188        } catch (\Throwable $e) {
189            return [
190                'meta' => ['success' => false, 'error' => $e->getMessage()],
191                'data' => null,
192            ];
193        }
194    }
195}