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