Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
80.90% |
72 / 89 |
|
50.00% |
3 / 6 |
CRAP | |
0.00% |
0 / 1 |
CaptchaService | |
80.90% |
72 / 89 |
|
50.00% |
3 / 6 |
19.01 | |
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 | |
71.43% |
30 / 42 |
|
0.00% |
0 / 1 |
8.14 |
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\Zmscitizenapi\Services\Captcha\CaptchaService; |
10 | use BO\Zmsentities\Schema\Entity; |
11 | use Firebase\JWT\JWT; |
12 | use GuzzleHttp\Client; |
13 | use GuzzleHttp\Exception\RequestException; |
14 | |
15 | class 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 | } |