commit 835051764080d090a3921678c85c9057b51315f4 Author: SLINIcraftet204 Date: Thu Apr 10 17:42:19 2025 +0200 initial commit, nothing working yet but added Debugger to basic informations tab or tried too diff --git a/README.md b/README.md new file mode 100644 index 0000000..4e4091c --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +## A repo clone for an Extension for [Shopware 6.6.X](https://github.com/shopware/platform) + +**Selfcoded and updated Version of MelvTurnstile**: Integrates CloudFlare Turnstile in Shopware with the Core Version 6.6.X. + +Create a free account or login to CloudFlare to generate your Sitekeys: +[CloudFlare Turnstile](https://www.cloudflare.com/products/turnstile/) + +**Config:** +![](https://i.imgur.com/qutsRPd.png) + +**Front-end:** +![](https://i.imgur.com/0qr655N.png) + +Known issues: +* Not working when form in modal +* No alert when captcha timed out +* Current captcha settings are overridden when installing plugin +* _to be continued..._ + +## Requirements + +| Version | Requirements | +|------------------------------------------------------------------------|---------------------------- | +|[0.1.0](https://github.com/SLINIcraftet204/MelvTurnstile/releases) | Shopware 6.4 >= | +| 0.2.0 (coming soon) | Shopware 6.6 >= | + +## Logo + +Logo by [Seeklogo.com](https://seeklogo.com/vector-logo/453922/cloudflare-turnstile) diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..7977627 --- /dev/null +++ b/composer.json @@ -0,0 +1,31 @@ +{ + "name": "melvinachterhuis/turnstile-shopware6-plugin", + "description": "CloudFlare Turnstile Captcha", + "version": "0.1.04-alpha", + "type": "shopware-platform-plugin", + "license": "MIT", + "authors": [ + { + "name": "Melvin Achterhuis" + }, + { + "name": "SLINIcraftet204" + } + ], + "require": { + "shopware/core": "6.6.*", + "ext-curl": "*" + }, + "autoload": { + "psr-4": { + "TurnstileReMaster\\": "src/" + } + }, + "extra": { + "shopware-plugin-class": "TurnstileReMaster\\MelvTurnstile", + "label": { + "de-DE": "Turnstile-Remaster (Canary)", + "en-GB": "Turnstile-Remaster (Canary)" + } + } +} diff --git a/src/MelvTurnstile.php b/src/MelvTurnstile.php new file mode 100644 index 0000000..eb56f6c --- /dev/null +++ b/src/MelvTurnstile.php @@ -0,0 +1,9 @@ + + */ + private function getCloudflareDefaultConfig(): array + { + return [ + 'name' => self::CLOUDFLARE_CAPTCHA_KEY, // Oder 'Cloudflare Turnstile' für den Anzeigenamen + 'isActive' => false, // Standardmäßig deaktiviert + 'config' => [ + 'siteKey' => '', + 'secretKey' => '', + ], + ]; + } + + public function update(Connection $connection): void + { + // Hole die aktuelle globale Konfiguration (sales_channel_id IS NULL) + $currentConfig = $connection->fetchAssociative( + 'SELECT id, configuration_value FROM system_config WHERE configuration_key = :key AND sales_channel_id IS NULL LIMIT 1', + ['key' => self::CONFIG_KEY] + ); + + // Fall 1: Der Basiseintrag für Captchas existiert noch gar nicht (sehr unwahrscheinlich im Core, aber sicher ist sicher) + if ($currentConfig === false) { + // Erstelle einen neuen Eintrag mit allen Captchas (dies sollte selten passieren) + // Holen der Standard-Captchas von Shopware wäre hier besser, aber für den Moment erstellen wir nur mit unserem + $allCaptchas = $this->getDefaultShopwareCaptchas(); // Hole Standard-Captchas (siehe Helper unten) + $allCaptchas[self::CLOUDFLARE_CAPTCHA_KEY] = $this->getCloudflareDefaultConfig(); + + $connection->insert('system_config', [ + 'id' => Uuid::randomBytes(), + 'configuration_key' => self::CONFIG_KEY, + 'configuration_value' => json_encode(['_value' => $allCaptchas]), + 'sales_channel_id' => null, + 'created_at' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT), + ]); + return; // Fertig + } + + $configId = $currentConfig['id']; + $currentValueJson = $currentConfig['configuration_value']; + + // Fall 2: Eintrag existiert, aber Wert ist leer oder ungültig + if (empty($currentValueJson)) { + $configValue = ['_value' => []]; // Start mit leerem Array + } else { + $configValue = json_decode($currentValueJson, true); + // Prüfen, ob JSON-Dekodierung fehlschlug oder die Struktur nicht passt + if (json_last_error() !== JSON_ERROR_NONE || !isset($configValue['_value']) || !is_array($configValue['_value'])) { + // Logge einen Fehler oder überschreibe mit sinnvollem Standard? Fürs Erste überspringen wir. + // Alternativ: $configValue = ['_value' => []]; // Reset bei ungültiger Struktur + // Oder wir loggen es und tun nichts: error_log('Invalid JSON structure for ' . self::CONFIG_KEY); + return; // Änderung nicht sicher möglich + } + } + + // Fall 3: Eintrag existiert und ist gültig - Prüfen, ob unser Captcha schon da ist + if (isset($configValue['_value'][self::CLOUDFLARE_CAPTCHA_KEY])) { + // Unser Captcha ist bereits vorhanden, nichts zu tun. + return; + } + + // Fall 4: Unser Captcha hinzufügen + $configValue['_value'][self::CLOUDFLARE_CAPTCHA_KEY] = $this->getCloudflareDefaultConfig(); + + // Die aktualisierte Konfiguration zurück in die Datenbank schreiben + $connection->update( + 'system_config', + [ + // JSON mit korrektem Wrapping (_value) speichern + 'configuration_value' => json_encode($configValue), + // Wichtig: updated_at setzen, nicht created_at! + 'updated_at' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT), + ], + [ + // Sicherstellen, dass wir den korrekten Datensatz aktualisieren + 'id' => $configId, + ] + ); + } + + /** + * Helper, um die Standard-Captcha-Liste zu bekommen (vereinfacht). + * In einer echten Migration könnte man versuchen, diese dynamischer zu laden, + * aber für den Zweck der Migration ist eine feste Liste oft ausreichend. + * WICHTIG: Diese Liste muss ggf. an die Ziel-Shopware-Version angepasst werden! + * Prüfe die `shopware/core/System/SystemConfig/Service/ConfigurationService.php` + * oder ähnliche Core-Dateien der Zielversion für die korrekten Standard-Captchas. + * + * @return array + */ + private function getDefaultShopwareCaptchas(): array + { + // Diese Liste basiert auf deiner alten Migration und muss ggf. für SW 6.6 geprüft/angepasst werden! + // Shopware 6.6 enthält möglicherweise andere/mehr Standard-Captchas. + return [ + 'honeypot' => [ + 'name' => 'Honeypot', + 'isActive' => true, // Honeypot ist oft standardmäßig aktiv + ], + 'basicCaptcha' => [ + 'name' => 'basicCaptcha', + 'isActive' => false, + ], + 'googleReCaptchaV2' => [ + 'name' => 'googleReCaptchaV2', + 'isActive' => false, + 'config' => [ + 'siteKey' => '', + 'secretKey' => '', + 'invisible' => false, + ], + ], + 'googleReCaptchaV3' => [ + 'name' => 'googleReCaptchaV3', + 'isActive' => false, + 'config' => [ + 'siteKey' => '', + 'secretKey' => '', + 'thresholdScore' => 0.5, + ], + ], + // Füge hier ggf. weitere Standard-Captchas von SW 6.6 hinzu + ]; + } + + + public function updateDestructive(Connection $connection): void + { + // Implementiere, falls nötig (z.B. Entfernen alter Konfigurationen) + // Normalerweise hier nichts zu tun für das Hinzufügen eines Captchas. + } +} \ No newline at end of file diff --git a/src/Resources/app/administration/src/main.js b/src/Resources/app/administration/src/main.js new file mode 100644 index 0000000..e53e55e --- /dev/null +++ b/src/Resources/app/administration/src/main.js @@ -0,0 +1 @@ +import './module/sw-settings-basic-information/component/sw-settings-captcha-select-v2'; \ No newline at end of file diff --git a/src/Resources/app/administration/src/module/sw-settings-basic-information/component/sw-settings-captcha-select-v2/index.js b/src/Resources/app/administration/src/module/sw-settings-basic-information/component/sw-settings-captcha-select-v2/index.js new file mode 100644 index 0000000..4ae0a8f --- /dev/null +++ b/src/Resources/app/administration/src/module/sw-settings-basic-information/component/sw-settings-captcha-select-v2/index.js @@ -0,0 +1,108 @@ +// src/Resources/app/administration/src/module/sw-settings-basic-information/component/sw-settings-captcha-select-v2/index.js + +import template from './sw-settings-captcha-select-v2.html.twig'; +import enGB from './snippet/en-GB.json'; +import deDE from './snippet/de-DE.json'; + +const { Component, Locale } = Shopware; + +Locale.extend('en-GB', enGB); +Locale.extend('de-DE', deDE); + +console.info('DEBUG Turnstile Plugin: Overriding sw-settings-captcha-select-v2 component...'); + +Component.override('sw-settings-captcha-select-v2', { + template, + + // Hinzufügen eines 'created' Hooks für frühes Debugging + created() { + console.log('DEBUG Turnstile Plugin: Component created. Initial currentValue:', this.currentValue); + console.log('DEBUG Turnstile Plugin: Initial allCaptchaTypes:', this.allCaptchaTypes); + // Rufen Sie die ursprüngliche created-Methode auf, falls vorhanden (gute Praxis) + if (typeof this.$super('created') === 'function') { + this.$super('created'); + } + }, + + computed: { + captchaOptions() { + console.log('--- DEBUG Turnstile Plugin: Computing captchaOptions ---'); + // WICHTIG: Logge den Zustand von allCaptchaTypes HIER, da computed properties reaktiv sind + console.log('DEBUG Turnstile Plugin: this.allCaptchaTypes inside computed:', this.allCaptchaTypes); + + // Verwende die Helper-Methode, um Basisoptionen zu bauen + const options = this.buildBaseCaptchaOptions(); + console.log('DEBUG Turnstile Plugin: Base options built:', JSON.parse(JSON.stringify(options))); // Deep copy for logging + + // Prüfen, ob unser Typ in den geladenen Daten existiert + if (this.allCaptchaTypes && this.allCaptchaTypes.cloudFlareTurnstile) { + console.log('DEBUG Turnstile Plugin: "cloudFlareTurnstile" FOUND in this.allCaptchaTypes. Adding to options.'); + options.push({ + value: 'cloudFlareTurnstile', + label: this.$tc('sw-settings-basic-information.captcha.label.cloudFlareTurnstile'), + }); + } else { + // SEHR WICHTIG: Wenn diese Meldung erscheint, fehlt dein Captcha in den Daten! + console.warn('DEBUG Turnstile Plugin: "cloudFlareTurnstile" ***NOT FOUND*** in this.allCaptchaTypes!'); + // Logge, was stattdessen vorhanden ist + if (this.allCaptchaTypes) { + console.warn('DEBUG Turnstile Plugin: Available keys in allCaptchaTypes:', Object.keys(this.allCaptchaTypes)); + } else { + console.warn('DEBUG Turnstile Plugin: this.allCaptchaTypes is null or undefined.'); + } + } + + // Sortieren (optional) + options.sort((a, b) => a.label.localeCompare(b.label)); + + console.log('DEBUG Turnstile Plugin: Final options returned:', JSON.parse(JSON.stringify(options))); // Deep copy for logging + console.log('--- DEBUG Turnstile Plugin: Finished computing captchaOptions ---'); + return options; + }, + + // Helper - unverändert lassen oder ggf. an SW 6.6 anpassen + buildBaseCaptchaOptions() { + // ... (Code aus der vorherigen Antwort - hier zur Kürze weggelassen) ... + // Stelle sicher, dass dieser Teil die Standard-Captchas korrekt generiert + // Basierend auf this.allCaptchaTypes + const options = []; + if (!this.allCaptchaTypes) { + console.warn('DEBUG Turnstile Plugin (buildBaseCaptchaOptions): this.allCaptchaTypes is missing!'); + return []; + } + if (this.allCaptchaTypes.honeypot) options.push({ value: 'honeypot', label: this.$tc('sw-settings-basic-information.captcha.label.honeypot') }); + if (this.allCaptchaTypes.basicCaptcha) options.push({ value: 'basicCaptcha', label: this.$tc('sw-settings-basic-information.captcha.label.basicCaptcha') }); + if (this.allCaptchaTypes.googleReCaptchaV2) options.push({ value: 'googleReCaptchaV2', label: this.$tc('sw-settings-basic-information.captcha.label.googleReCaptchaV2') }); + if (this.allCaptchaTypes.googleReCaptchaV3) options.push({ value: 'googleReCaptchaV3', label: this.$tc('sw-settings-basic-information.captcha.label.googleReCaptchaV3') }); + return options; + } + }, + + // Optional: Hinzufügen von Methoden zum Stringifizieren für das Template-Debugging + methods: { + stringifyForDebug(value) { + // Einfache Stringifizierung für das Template, um Zirkelbezüge zu vermeiden + try { + // Nur Top-Level-Keys anzeigen, um Überladung zu vermeiden + if (value && typeof value === 'object') { + return JSON.stringify(Object.keys(value)); + } + return JSON.stringify(value); + } catch (e) { + return '[Error stringifying]'; + } + }, + getCloudflareTurnstileDataForDebug() { + if (this.allCaptchaTypes && this.allCaptchaTypes.cloudFlareTurnstile) { + try { + return JSON.stringify(this.allCaptchaTypes.cloudFlareTurnstile, null, 2); + } catch (e) { + return '[Error stringifying cloudFlareTurnstile data]'; + } + } + return 'Not found in allCaptchaTypes'; + } + } +}); + +console.info('DEBUG Turnstile Plugin: Override applied.'); \ No newline at end of file diff --git a/src/Resources/app/administration/src/module/sw-settings-basic-information/component/sw-settings-captcha-select-v2/sw-settings-captcha-select-v2.html.twig b/src/Resources/app/administration/src/module/sw-settings-basic-information/component/sw-settings-captcha-select-v2/sw-settings-captcha-select-v2.html.twig new file mode 100644 index 0000000..b8fc22c --- /dev/null +++ b/src/Resources/app/administration/src/module/sw-settings-basic-information/component/sw-settings-captcha-select-v2/sw-settings-captcha-select-v2.html.twig @@ -0,0 +1,51 @@ + +{% block sw_settings_captcha_select_v2_google_recaptcha_v2 %} + {% parent() %} + {% block sw_settings_captcha_select_v2_cloudflare_turnstile %} + {% block sw_settings_captcha_select_v2_debug_output_turnstile %} + + +
+

1. Template Override active: YES

+

2. Component Data (allCaptchaTypes): {{ allCaptchaTypes ? 'Exists' : 'MISSING!' }}

+

3. Keys in allCaptchaTypes: {{ stringifyForDebug(allCaptchaTypes) }}

+

4. cloudFlareTurnstile in allCaptchaTypes: {{ (allCaptchaTypes && allCaptchaTypes.cloudFlareTurnstile) ? 'YES' : 'NO!' }}

+

5. Raw cloudFlareTurnstile Data:

+
{{ getCloudflareTurnstileDataForDebug() }}
+

6. Generated Options Count: {{ captchaOptions ? captchaOptions.length : 'N/A' }}

+

7. Generated Options Values: {{ captchaOptions ? stringifyForDebug(captchaOptions.map(opt => opt.value)) : 'N/A' }}

+
+
+ {% endblock %} + + + + {% block sw_settings_captcha_select_v2_cloudflare_turnstile_description %} +

+ {{ $tc('sw-settings-basic-information.captcha.label.cloudFlareTurnstileDescription') }} +

+ {% endblock %} + + + {% block sw_settings_captcha_select_v2_cloudflare_turnstile_site_key %} + + {% endblock %} + + + {% block sw_settings_captcha_select_v2_cloudflare_turnstile_secret_key %} + + {% endblock %} +
+ {% endblock %} +{% endblock %} \ No newline at end of file diff --git a/src/Resources/config/plugin.png b/src/Resources/config/plugin.png new file mode 100644 index 0000000..eaa433f Binary files /dev/null and b/src/Resources/config/plugin.png differ diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml new file mode 100644 index 0000000..0902b64 --- /dev/null +++ b/src/Resources/config/services.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/Resources/public/administration/js/melv-turnstile.js b/src/Resources/public/administration/js/melv-turnstile.js new file mode 100644 index 0000000..8989788 --- /dev/null +++ b/src/Resources/public/administration/js/melv-turnstile.js @@ -0,0 +1 @@ +!function(e){var t={};function n(l){if(t[l])return t[l].exports;var r=t[l]={i:l,l:!1,exports:{}};return e[l].call(r.exports,r,r.exports,n),r.l=!0,r.exports}n.m=e,n.c=t,n.d=function(e,t,l){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:l})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var l=Object.create(null);if(n.r(l),Object.defineProperty(l,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)n.d(l,r,function(t){return e[t]}.bind(null,r));return l},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/bundles/melvturnstile/",n(n.s="wNoc")}({"+t2p":function(e){e.exports=JSON.parse('{"sw-settings-basic-information":{"captcha":{"label":{"cloudFlareTurnstile":"CloudFlare Turnstile","cloudFlareTurnstileSiteKey":"CloudFlare Turnstile site key","cloudFlareTurnstileSecretKey":"CloudFlare Turnstile secret key","cloudFlareTurnstileDescription":"Turnstile is CloudFlare\'s CAPTCHA alternative. It automatically chooses from a rotating suite of non-intrusive browser challenges based on telemetry and client behavior exhibited during a session."}}}}')},"R+yN":function(e){e.exports=JSON.parse('{"sw-settings-basic-information":{"captcha":{"label":{"cloudFlareTurnstile":"CloudFlare Turnstile","cloudFlareTurnstileSiteKey":"CloudFlare Turnstile site key","cloudFlareTurnstileSecretKey":"CloudFlare Turnstile secret key","cloudFlareTurnstileDescription":"Turnstile is CloudFlare\'s CAPTCHA alternative. It automatically chooses from a rotating suite of non-intrusive browser challenges based on telemetry and client behavior exhibited during a session."}}}}')},wNoc:function(e,t,n){"use strict";n.r(t);var l=n("+t2p"),r=n("R+yN"),s=Shopware,i=s.Component,c=s.Locale;c.extend("en-GB",l),c.extend("de-DE",r),i.override("sw-settings-captcha-select-v2",{template:'\n{% block sw_settings_captcha_select_v2_google_recaptcha_v2 %}\n {% parent() %}\n {% block sw_settings_captcha_select_v2_cloudflare_turnstile %}\n \n\n \n {% block sw_settings_captcha_select_v2_cloudflare_turnstile_description %}\n

\n {{ $tc(\'sw-settings-basic-information.captcha.label.cloudFlareTurnstileDescription\') }}\n

\n {% endblock %}\n\n \n {% block sw_settings_captcha_select_v2_cloudflare_turnstile_site_key %}\n \n {% endblock %}\n\n \n {% block sw_settings_captcha_select_v2_cloudflare_turnstile_secret_key %}\n \n {% endblock %}\n \n {% endblock %}\n{% endblock %}'})}}); \ No newline at end of file diff --git a/src/Resources/snippet/storefront.de-DE.json b/src/Resources/snippet/storefront.de-DE.json new file mode 100644 index 0000000..a585d78 --- /dev/null +++ b/src/Resources/snippet/storefront.de-DE.json @@ -0,0 +1,7 @@ +{ + "captcha": { + "cloudFlareTurnstile": { + "dataProtectionInformation": "This site is protected by Turnstile and the CloudFlare Privacy Policy and Terms of Service apply." + } + } +} \ No newline at end of file diff --git a/src/Resources/snippet/storefront.en-GB.json b/src/Resources/snippet/storefront.en-GB.json new file mode 100644 index 0000000..a585d78 --- /dev/null +++ b/src/Resources/snippet/storefront.en-GB.json @@ -0,0 +1,7 @@ +{ + "captcha": { + "cloudFlareTurnstile": { + "dataProtectionInformation": "This site is protected by Turnstile and the CloudFlare Privacy Policy and Terms of Service apply." + } + } +} \ No newline at end of file diff --git a/src/Resources/views/storefront/component/captcha/cloudFlareTurnstile.html.twig b/src/Resources/views/storefront/component/captcha/cloudFlareTurnstile.html.twig new file mode 100644 index 0000000..faf59a0 --- /dev/null +++ b/src/Resources/views/storefront/component/captcha/cloudFlareTurnstile.html.twig @@ -0,0 +1,9 @@ +{% block component_captcha_cloudflare_turnstile %} +
+ +
+ {{ "captcha.cloudFlareTurnstile.dataProtectionInformation"|trans|sw_sanitize }} +
+
+{% endblock %} diff --git a/src/Resources/views/storefront/component/recaptcha.html.twig b/src/Resources/views/storefront/component/recaptcha.html.twig new file mode 100644 index 0000000..19e187f --- /dev/null +++ b/src/Resources/views/storefront/component/recaptcha.html.twig @@ -0,0 +1,11 @@ +{% sw_extends '@Storefront/storefront/component/recaptcha.html.twig' %} + +{% block component_head_javascript_recaptcha %} + {{ parent() }} + {% block component_head_javascript_turnstile %} + {% set turnstileActive = config('core.basicInformation.activeCaptchasV2.cloudFlareTurnstile.isActive') %} + {% if turnstileActive %} + + {% endif %} + {% endblock %} +{% endblock %} \ No newline at end of file diff --git a/src/Storefront/Framework/Captcha/CloudFlareTurnstile.php b/src/Storefront/Framework/Captcha/CloudFlareTurnstile.php new file mode 100644 index 0000000..71d9686 --- /dev/null +++ b/src/Storefront/Framework/Captcha/CloudFlareTurnstile.php @@ -0,0 +1,120 @@ +client = $client; + $this->logger = $logger; + } + + /** + * {@inheritdoc} + * Validiert das Cloudflare Turnstile Captcha. + * Erwartet die Konfiguration im $captchaConfig Array. + */ + public function isValid(Request $request, array $captchaConfig): bool // Klare Signatur für SW >= 6.5 + { + $turnstileResponse = $request->request->get(self::CAPTCHA_REQUEST_PARAMETER); // Sicherer Zugriff auf POST-Parameter + + if (!\is_string($turnstileResponse) || $turnstileResponse === '') { + $this->logger->debug('Cloudflare Turnstile token missing in request.', ['request_params' => $request->request->all()]); + return false; + } + + // Hole Secret Key aus der Konfiguration + $secretKey = $captchaConfig['config']['secretKey'] ?? null; + + if (!\is_string($secretKey) || $secretKey === '') { + $this->logger->error('Cloudflare Turnstile secret key is missing or not configured in Shopware settings.'); + // Gib false zurück, da ohne Secret Key keine Validierung möglich ist. + // Alternativ könnte man hier eine Exception werfen, aber false ist im Sinne von isValid() oft besser. + return false; + } + + try { + $response = $this->client->request('POST', self::CLOUDFLARE_CAPTCHA_VERIFY_ENDPOINT, [ + 'form_params' => [ + 'secret' => $secretKey, + 'response' => $turnstileResponse, + 'remoteip' => $request->getClientIp(), // Client IP mitsenden ist optional aber empfohlen + ], + 'timeout' => 5, // Timeout hinzufügen, um lange Wartezeiten zu vermeiden + ]); + + $responseBody = $response->getBody()->getContents(); + $responseData = json_decode($responseBody, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + $this->logger->warning('Failed to decode Cloudflare Turnstile verify response.', ['response_body' => $responseBody]); + return false; + } + + if (!isset($responseData['success'])) { + $this->logger->warning('Cloudflare Turnstile verify response missing "success" key.', ['response_data' => $responseData]); + return false; + } + + if ($responseData['success'] === true) { + return true; // Captcha ist gültig + } + + // Logge Fehlercodes von Cloudflare, falls vorhanden + $errorCodes = $responseData['error-codes'] ?? []; + $this->logger->info('Cloudflare Turnstile validation failed.', [ + 'success' => false, + 'error-codes' => $errorCodes, + 'clientIp' => $request->getClientIp() // Logge relevante Infos + ]); + return false; // Captcha ist ungültig + + } catch (ClientExceptionInterface $e) { + // Fehler bei der HTTP-Anfrage an Cloudflare (Netzwerkproblem, Timeout etc.) + $this->logger->error('HTTP error during Cloudflare Turnstile verification.', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() // Optional für Debugging + ]); + return false; // Bei Fehlern als ungültig betrachten + } catch (Throwable $e) { + // Fange andere mögliche Fehler ab (z.B. Guzzle-Fehler, JSON-Fehler etc.) + $this->logger->error('Unexpected error during Cloudflare Turnstile verification.', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + return false; + } + } + + /** + * {@inheritdoc} + */ + public function getName(): string + { + return self::CAPTCHA_NAME; + } +} \ No newline at end of file