initial commit, nothing working yet but added Debugger to basic informations tab or tried too

This commit is contained in:
Nils 2025-04-10 17:42:19 +02:00
commit 8350517640
Signed by: slinicraftet204
GPG Key ID: 78E12696BAFC2A4B
15 changed files with 555 additions and 0 deletions

29
README.md Normal file
View File

@ -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)

31
composer.json Normal file
View File

@ -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)"
}
}
}

9
src/MelvTurnstile.php Normal file
View File

@ -0,0 +1,9 @@
<?php declare(strict_types=1);
namespace TurnstileReMaster;
use Shopware\Core\Framework\Plugin;
class MelvTurnstile extends Plugin
{
}

View File

@ -0,0 +1,158 @@
<?php declare(strict_types=1);
namespace TurnstileReMaster\Migration; // Stelle sicher, dass der Namespace korrekt ist
use Doctrine\DBAL\Connection;
use Shopware\Core\Defaults;
use Shopware\Core\Framework\Migration\MigrationStep;
use Shopware\Core\Framework\Uuid\Uuid; // Importieren für `created_at` falls nicht vorhanden
use Shopware\Core\System\SystemConfig\SystemConfigService; // Kann nützlich sein, aber hier reicht Connection
class Migration1664374217addTurnStileCaptcha extends MigrationStep
{
// Der System-Config Key für die Captchas
private const CONFIG_KEY = 'core.basicInformation.activeCaptchasV2';
// Der eindeutige Key für unser Captcha innerhalb der Konfiguration
private const CLOUDFLARE_CAPTCHA_KEY = 'cloudFlareTurnstile';
public function getCreationTimestamp(): int
{
return 1664374217; // Behalte den ursprünglichen Timestamp bei
}
/**
* Gibt die Standardkonfiguration für Cloudflare Turnstile zurück.
*
* @return array<string, mixed>
*/
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<string, mixed>
*/
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.
}
}

View File

@ -0,0 +1 @@
import './module/sw-settings-basic-information/component/sw-settings-captcha-select-v2';

View File

@ -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.');

View File

@ -0,0 +1,51 @@
<!-- eslint-disable-next-line sw-deprecation-rules/no-twigjs-blocks -->
{% 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 %}
<sw-alert variant="info" :showIcon="true" style="margin-bottom: 20px;">
<template #title>Debug Info (Turnstile Plugin)</template>
<div style="font-family: monospace; white-space: pre-wrap; word-break: break-all; max-height: 300px; overflow: auto; font-size: 12px;">
<p><strong>1. Template Override active:</strong> YES</p>
<p><strong>2. Component Data (allCaptchaTypes):</strong> {{ allCaptchaTypes ? 'Exists' : 'MISSING!' }}</p>
<p><strong>3. Keys in allCaptchaTypes:</strong> {{ stringifyForDebug(allCaptchaTypes) }}</p>
<p><strong>4. cloudFlareTurnstile in allCaptchaTypes:</strong> {{ (allCaptchaTypes && allCaptchaTypes.cloudFlareTurnstile) ? 'YES' : 'NO!' }}</p>
<p><strong>5. Raw cloudFlareTurnstile Data:</strong></p>
<pre>{{ getCloudflareTurnstileDataForDebug() }}</pre>
<p><strong>6. Generated Options Count:</strong> {{ captchaOptions ? captchaOptions.length : 'N/A' }}</p>
<p><strong>7. Generated Options Values:</strong> {{ captchaOptions ? stringifyForDebug(captchaOptions.map(opt => opt.value)) : 'N/A' }}</p>
</div>
</sw-alert>
{% endblock %}
<sw-container
v-if="currentValue.cloudFlareTurnstile.isActive"
class="sw-settings-captcha-select-v2__cloudflare-turnstile"
>
<!-- eslint-disable-next-line sw-deprecation-rules/no-twigjs-blocks -->
{% block sw_settings_captcha_select_v2_cloudflare_turnstile_description %}
<p class="sw-settings-captcha-select-v2__description sw-settings-captcha-select-v2__cloudflare-turnstile-description">
{{ $tc('sw-settings-basic-information.captcha.label.cloudFlareTurnstileDescription') }}
</p>
{% endblock %}
<!-- eslint-disable-next-line sw-deprecation-rules/no-twigjs-blocks -->
{% block sw_settings_captcha_select_v2_cloudflare_turnstile_site_key %}
<sw-text-field
v-model="currentValue.cloudFlareTurnstile.config.siteKey"
name="cloudFlareTurnstileSiteKey"
:label="$tc('sw-settings-basic-information.captcha.label.cloudFlareTurnstileSiteKey')"
/>
{% endblock %}
<!-- eslint-disable-next-line sw-deprecation-rules/no-twigjs-blocks -->
{% block sw_settings_captcha_select_v2_cloudflare_turnstile_secret_key %}
<sw-text-field
v-model="currentValue.cloudFlareTurnstile.config.secretKey"
name="cloudFlareTurnstileSecretKey"
:label="$tc('sw-settings-basic-information.captcha.label.cloudFlareTurnstileSecretKey')"
/>
{% endblock %}
</sw-container>
{% endblock %}
{% endblock %}

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -0,0 +1,13 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="TurnstileReMaster\Storefront\Framework\Captcha\CloudFlareTurnstile">
<argument type="service" id="shopware.captcha.client"/>
<tag name="shopware.captcha" priority="50"/>
</service>
</services>
</container>

View File

@ -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 <sw-container\n v-if="currentValue.cloudFlareTurnstile.isActive"\n class="sw-settings-captcha-select-v2__cloudflare-turnstile"\n >\n\n \n {% block sw_settings_captcha_select_v2_cloudflare_turnstile_description %}\n <p class="sw-settings-captcha-select-v2__description sw-settings-captcha-select-v2__cloudflare-turnstile-description">\n {{ $tc(\'sw-settings-basic-information.captcha.label.cloudFlareTurnstileDescription\') }}\n </p>\n {% endblock %}\n\n \n {% block sw_settings_captcha_select_v2_cloudflare_turnstile_site_key %}\n <sw-text-field\n v-model="currentValue.cloudFlareTurnstile.config.siteKey"\n name="cloudFlareTurnstileSiteKey"\n :label="$tc(\'sw-settings-basic-information.captcha.label.cloudFlareTurnstileSiteKey\')"\n />\n {% endblock %}\n\n \n {% block sw_settings_captcha_select_v2_cloudflare_turnstile_secret_key %}\n <sw-text-field\n v-model="currentValue.cloudFlareTurnstile.config.secretKey"\n name="cloudFlareTurnstileSecretKey"\n :label="$tc(\'sw-settings-basic-information.captcha.label.cloudFlareTurnstileSecretKey\')"\n />\n {% endblock %}\n </sw-container>\n {% endblock %}\n{% endblock %}'})}});

View File

@ -0,0 +1,7 @@
{
"captcha": {
"cloudFlareTurnstile": {
"dataProtectionInformation": "This site is protected by Turnstile and the CloudFlare <a href=\"https://www.cloudflare.com/en-gb/privacypolicy/\">Privacy Policy</a> and <a href=\"https://www.cloudflare.com/en-gb/website-terms/\">Terms of Service</a> apply."
}
}
}

View File

@ -0,0 +1,7 @@
{
"captcha": {
"cloudFlareTurnstile": {
"dataProtectionInformation": "This site is protected by Turnstile and the CloudFlare <a href=\"https://www.cloudflare.com/en-gb/privacypolicy/\">Privacy Policy</a> and <a href=\"https://www.cloudflare.com/en-gb/website-terms/\">Terms of Service</a> apply."
}
}
}

View File

@ -0,0 +1,9 @@
{% block component_captcha_cloudflare_turnstile %}
<div class="cf-turnstile mb-2"
data-sitekey="{{ config('core.basicInformation.activeCaptchasV2.cloudFlareTurnstile.config.siteKey') }}">
<div class="data-protection-information cf_turnstile-protection-information mb-2">
{{ "captcha.cloudFlareTurnstile.dataProtectionInformation"|trans|sw_sanitize }}
</div>
</div>
{% endblock %}

View File

@ -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 %}
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
{% endif %}
{% endblock %}
{% endblock %}

View File

@ -0,0 +1,120 @@
<?php declare(strict_types=1);
namespace TurnstileReMaster\Storefront\Framework\Captcha; // Stelle sicher, dass der Namespace korrekt ist
use GuzzleHttp\ClientInterface;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Log\LoggerInterface; // Logger hinzufügen
use Shopware\Storefront\Framework\Captcha\AbstractCaptcha;
use Symfony\Component\HttpFoundation\Request;
use Throwable; // Für generischere Exception-Behandlung
/**
* @internal // Beibehalten, wenn es eine interne Klasse des Plugins ist
*/
final class CloudFlareTurnstile extends AbstractCaptcha // 'final', wenn keine Ableitung geplant ist
{
public const CAPTCHA_NAME = 'cloudFlareTurnstile';
// Injected by CF in form
public const CAPTCHA_REQUEST_PARAMETER = 'cf-turnstile-response';
private const CLOUDFLARE_CAPTCHA_VERIFY_ENDPOINT = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
private ClientInterface $client;
private LoggerInterface $logger; // Logger-Instanz
/**
* @internal
*/
public function __construct(
ClientInterface $client,
LoggerInterface $logger // Logger injizieren
) {
$this->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;
}
}