Initial commit

This commit is contained in:
Melvin Achterhuis 2022-09-28 20:52:59 +02:00
commit c2c3078f6d
17 changed files with 341 additions and 0 deletions

26
README.md Normal file
View File

@ -0,0 +1,26 @@
## A plugin for [Shopware 6](https://github.com/shopware/platform)
**Proof-of-concept**: Integrates CloudFlare Turnstile with Shopware 6.
Create a free account to claim your keys: https://www.cloudflare.com/en-gb/lp/turnstile/
*Config:*
![](https://i.imgur.com/qutsRPd.png)
Known issues:
* Not working when form in modal
* No alert when captcha invalid
* Missing translations for German
* ..
## Requirements
| Version | Requirements |
|------------|---------------------------- |
| 0.1.0 | Shopware 6.4 >= |
## License
Plugin's Icon by [flaticon](https://www.flaticon.com).
The plugin is released under MIT. For a full overview check the [LICENSE](./LICENSE) file.

28
composer.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "melvinachterhuis/turnstile-shopware6-plugin",
"description": "CloudFlare Turnstile Captcha",
"version": "0.1.0",
"type": "shopware-platform-plugin",
"license": "MIT",
"authors": [
{
"name": "Melvin Achterhuis"
}
],
"require": {
"shopware/core": "6.4.*",
"ext-curl": "*"
},
"autoload": {
"psr-4": {
"Melv\\Turnstile\\": "src/"
}
},
"extra": {
"shopware-plugin-class": "Melv\\Turnstile\\MelvTurnstile",
"label": {
"de-DE": "CloudFlare Turnstile Captcha",
"en-GB": "CloudFlare Turnstile Captcha"
}
}
}

9
src/MelvTurnstile.php Normal file
View File

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

View File

@ -0,0 +1,80 @@
<?php declare(strict_types=1);
namespace Melv\Turnstile\Migration;
use Doctrine\DBAL\Connection;
use Shopware\Core\Defaults;
use Shopware\Core\Framework\Migration\MigrationStep;
class Migration1664374217addTurnStileCaptcha extends MigrationStep
{
private const CONFIG_KEY = 'core.basicInformation.activeCaptchasV2';
private array $captchaItems = [
'honeypot' => [
'name' => 'Honeypot',
'isActive' => false,
],
'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,
],
],
'cloudFlareTurnstile' => [
'name' => 'cloudFlareTurnstile',
'isActive' => false,
'config' => [
'siteKey' => '',
'secretKey' => ''
]
]
];
public function getCreationTimestamp(): int
{
return 1664374217;
}
public function update(Connection $connection): void
{
//TODO: Can we prevent overriding current CAPTCHA settings?
$configId = $connection->fetchColumn('SELECT id FROM system_config WHERE configuration_key = :key AND updated_at IS NULL', [
'key' => self::CONFIG_KEY,
]);
if (!$configId) {
return;
}
$connection->update('system_config', [
'configuration_key' => self::CONFIG_KEY,
'configuration_value' => json_encode(['_value' => $this->captchaItems]),
'created_at' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
], [
'id' => $configId,
]);
}
public function updateDestructive(Connection $connection): void
{
// implement update destructive
}
}

View File

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

View File

@ -0,0 +1,12 @@
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);
Component.override('sw-settings-captcha-select-v2', {
template,
});

View File

@ -0,0 +1,12 @@
{
"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."
}
}
}
}

View File

@ -0,0 +1,12 @@
{
"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."
}
}
}
}

View File

@ -0,0 +1,36 @@
<!-- 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 %}
<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: 5.1 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="Melv\Turnstile\Storefront\Framework\Captcha\CloudFlareTurnstile">
<argument type="service" id="shopware.captcha.client"/>
<tag name="shopware.storefront.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,77 @@
<?php declare(strict_types=1);
namespace Melv\Turnstile\Storefront\Framework\Captcha;
use GuzzleHttp\ClientInterface;
use Psr\Http\Client\ClientExceptionInterface;
use Shopware\Core\Framework\Feature;
use Shopware\Storefront\Framework\Captcha\AbstractCaptcha;
use Symfony\Component\HttpFoundation\Request;
class CloudFlareTurnstile extends AbstractCaptcha
{
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;
/**
* @internal
*/
public function __construct(ClientInterface $client)
{
$this->client = $client;
}
/**
* {@inheritdoc}
*/
public function isValid(Request $request /* , array $captchaConfig */): bool
{
if (\func_num_args() < 2 || !\is_array(func_get_arg(1))) {
Feature::triggerDeprecationOrThrow(
'v6.5.0.0',
'Method `isValid()` in `CloudFlareTurnstile` expects passing the `$captchaConfig` as array as the second parameter in v6.5.0.0.'
);
}
if (!$request->get(self::CAPTCHA_REQUEST_PARAMETER)) {
return false;
}
$captchaConfig = \func_get_args()[1] ?? [];
$secretKey = !empty($captchaConfig['config']['secretKey']) ? $captchaConfig['config']['secretKey'] : null;
if (!\is_string($secretKey)) {
return false;
}
try {
$response = $this->client->request('POST', self::CLOUDFLARE_CAPTCHA_VERIFY_ENDPOINT, [
'form_params' => [
'secret' => $secretKey,
'response' => $request->get(self::CAPTCHA_REQUEST_PARAMETER),
'remoteip' => $request->getClientIp(),
],
]);
$responseRaw = $response->getBody()->getContents();
$response = json_decode($responseRaw, true);
return $response && (bool) $response['success'];
} catch (ClientExceptionInterface $exception) {
return false;
}
}
/**
* {@inheritdoc}
*/
public function getName(): string
{
return self::CAPTCHA_NAME;
}
}