From 835051764080d090a3921678c85c9057b51315f4 Mon Sep 17 00:00:00 2001 From: SLINIcraftet204 Date: Thu, 10 Apr 2025 17:42:19 +0200 Subject: [PATCH] initial commit, nothing working yet but added Debugger to basic informations tab or tried too --- README.md | 29 ++++ composer.json | 31 ++++ src/MelvTurnstile.php | 9 + ...Migration1664374217addTurnStileCaptcha.php | 158 ++++++++++++++++++ src/Resources/app/administration/src/main.js | 1 + .../sw-settings-captcha-select-v2/index.js | 108 ++++++++++++ .../sw-settings-captcha-select-v2.html.twig | 51 ++++++ src/Resources/config/plugin.png | Bin 0 -> 37857 bytes src/Resources/config/services.xml | 13 ++ .../administration/js/melv-turnstile.js | 1 + src/Resources/snippet/storefront.de-DE.json | 7 + src/Resources/snippet/storefront.en-GB.json | 7 + .../captcha/cloudFlareTurnstile.html.twig | 9 + .../storefront/component/recaptcha.html.twig | 11 ++ .../Framework/Captcha/CloudFlareTurnstile.php | 120 +++++++++++++ 15 files changed, 555 insertions(+) create mode 100644 README.md create mode 100644 composer.json create mode 100644 src/MelvTurnstile.php create mode 100644 src/Migration/Migration1664374217addTurnStileCaptcha.php create mode 100644 src/Resources/app/administration/src/main.js create mode 100644 src/Resources/app/administration/src/module/sw-settings-basic-information/component/sw-settings-captcha-select-v2/index.js create mode 100644 src/Resources/app/administration/src/module/sw-settings-basic-information/component/sw-settings-captcha-select-v2/sw-settings-captcha-select-v2.html.twig create mode 100644 src/Resources/config/plugin.png create mode 100644 src/Resources/config/services.xml create mode 100644 src/Resources/public/administration/js/melv-turnstile.js create mode 100644 src/Resources/snippet/storefront.de-DE.json create mode 100644 src/Resources/snippet/storefront.en-GB.json create mode 100644 src/Resources/views/storefront/component/captcha/cloudFlareTurnstile.html.twig create mode 100644 src/Resources/views/storefront/component/recaptcha.html.twig create mode 100644 src/Storefront/Framework/Captcha/CloudFlareTurnstile.php 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 0000000000000000000000000000000000000000..eaa433fd3a969cc4f522707caf72eb0ee96ebfc5 GIT binary patch literal 37857 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 S(r1mV7gpA~ntR#u;r|1DsmYB1 literal 0 HcmV?d00001 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