From fb958a735d74957d39f403d6d905b6fcbe341e06 Mon Sep 17 00:00:00 2001 From: "y2ksoft@gmail.com" Date: Tue, 27 Jun 2023 14:17:07 +1200 Subject: [PATCH 1/7] peer review changes --- README.md | 6 +- composer.json | 2 +- lang/de.yml | 2 +- lang/en.yml | 2 +- lang/fr.yml | 2 +- lang/ru.yml | 2 +- src/Forms/HttpClient.php | 26 ++++++ src/Forms/TurnstileCaptchaField.php | 91 +++++++------------ src/Forms/TurnstileCaptchaProtector.php | 2 +- templates/README.md | 7 -- .../Forms/TurnstileCaptchaField.ss | 2 +- 11 files changed, 71 insertions(+), 73 deletions(-) create mode 100644 src/Forms/HttpClient.php delete mode 100644 templates/README.md diff --git a/README.md b/README.md index 3232f8b..fb26499 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Adds a "spam protection" field to SilverStripe userforms using Cloudflare's ## Requirements * SilverStripe 5.x * [SilverStripe Spam Protection - 3.x](https://github.com/silverstripe/silverstripe-spamprotection/) + 4.x](https://github.com/silverstripe/silverstripe-spamprotection/) * PHP CURL ## Installation @@ -31,8 +31,8 @@ $form->enableSpamProtection(); ## Configuration There are multiple configuration options for the field, you must set the -site_key and the secret_key which you can get from the [reCAPTCHA -page](https://www.google.com/recaptcha). These configuration options must be +site_key and the secret_key which you can get from the [turnstile +page](https://developers.cloudflare.com/turnstile/). These configuration options must be added to your site's yaml config typically this is mysite/\_config/config.yml. ```yml Terraformers\TurnstileCaptcha\Forms\TurnstileCaptchaField: diff --git a/composer.json b/composer.json index f9a89b3..cdfb48b 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "require": { "php": "^8.1", "silverstripe/framework": "^5.0", - "silverstripe/spamprotection": "^3 | ^4" + "silverstripe/spamprotection": "^4" }, "autoload": { "psr-4": { diff --git a/lang/de.yml b/lang/de.yml index 298fe40..f9fda4d 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -1,4 +1,4 @@ de: - Terraformers\TurnstileCaptcha\Forms\CaptchaField: + Terraformers\TurnstileCaptcha\Forms\TurnstileCaptchaField: NOSCRIPT: "Sie müssen JavaScript aktivieren um dieses Formular zu übermitteln" VALIDATE_ERROR: "Spam-Schutz konnte nicht geprüft werden" diff --git a/lang/en.yml b/lang/en.yml index d1e09e2..41985f4 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -1,4 +1,4 @@ en: - Terraformers\TurnstileCaptcha\Forms\CaptchaField: + Terraformers\TurnstileCaptcha\Forms\TurnstileCaptchaField: NOSCRIPT: "You must enable JavaScript to submit this form" VALIDATE_ERROR: "Captcha could not be validated" diff --git a/lang/fr.yml b/lang/fr.yml index ac1762d..b4e2261 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -1,4 +1,4 @@ fr: - Terraformers\TurnstileCaptcha\Forms\CaptchaField: + Terraformers\TurnstileCaptcha\Forms\TurnstileCaptchaField: NOSCRIPT: "Vous devez activer JavaScript pour soumettre ce formulaire" VALIDATE_ERROR: "Captcha n'a pas pu être validé" diff --git a/lang/ru.yml b/lang/ru.yml index 2731257..ea7e61d 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -1,4 +1,4 @@ ru: - Terraformers\TurnstileCaptcha\Forms\CaptchaField: + Terraformers\TurnstileCaptcha\Forms\TurnstileCaptchaField: NOSCRIPT: "Укажите ответ на капчу, если вы её не видите вам необходимо включить JavaScript." VALIDATE_ERROR: "Ошибка проверки капчи - попробуйте ещё раз." diff --git a/src/Forms/HttpClient.php b/src/Forms/HttpClient.php new file mode 100644 index 0000000..4fde4bd --- /dev/null +++ b/src/Forms/HttpClient.php @@ -0,0 +1,26 @@ +client = $client; + } + + public function getClient(): Client + { + return $this->client; + } +} diff --git a/src/Forms/TurnstileCaptchaField.php b/src/Forms/TurnstileCaptchaField.php index 2c5c19d..8b4fc29 100644 --- a/src/Forms/TurnstileCaptchaField.php +++ b/src/Forms/TurnstileCaptchaField.php @@ -14,38 +14,38 @@ class TurnstileCaptchaField extends FormField { /** * Recaptcha Site Key - * @config NocaptchaField.site_key + * @config TurnstileCaptchaField.site_key */ private static ?string $site_key = null; /** * Recaptcha Secret Key - * @config NocaptchaField.secret_key + * @config TurnstileCaptchaField.secret_key */ private static ?string $secret_key = null; /** * CURL Proxy Server location - * @config NocaptchaField.proxy_server + * @config TurnstileCaptchaField.proxy_server */ private static ?string $proxy_server = null; /** * CURL Proxy authentication - * @config NocaptchaField.proxy_auth + * @config TurnstileCaptchaField.proxy_auth */ private static ?string $proxy_auth = null; /** * CURL Proxy port - * @config NocaptchaField.proxy_port + * @config TurnstileCaptchaField.proxy_port */ private static $proxy_port; /** * Verify SSL Certificates - * @config NocaptchaField.verify_ssl + * @config TurnstileCaptchaField.verify_ssl * @default true */ private static bool $verify_ssl = true; @@ -65,13 +65,13 @@ class TurnstileCaptchaField extends FormField private static bool $default_handle_submit = true; /** - * Recaptcha Site Key + * TurnstileCaptcha Site Key * Configurable via Injector config */ protected ?string $_siteKey = null; /** - * Recaptcha Site Key + * TurnstileCaptcha Site Key * Configurable via Injector config */ protected ?string $_secretKey = null; @@ -118,7 +118,7 @@ class TurnstileCaptchaField extends FormField private bool $handleSubmitEvents; /** - * Creates a new Recaptcha 2 field. + * Creates a new TurnstileCaptcha 2 field. * @param string $name The internal field name, passed to forms. * @param string $title The human-readable field label. * @param mixed $value The value of the field (unused) @@ -144,7 +144,7 @@ public function Field($properties = array()) $secretKey = $this->_secretKey ? $this->_secretKey : self::config()->secret_key; if (empty($siteKey) || empty($secretKey)) { - user_error('You must configure Nocaptcha.site_key and Nocaptcha.secret_key, you can retrieve these at https://google.com/recaptcha', E_USER_ERROR); + user_error('You must configure site_key and secret_key, you can retrieve these at https://developers.cloudflare.com/turnstile/', E_USER_ERROR); } Requirements::javascript( @@ -159,7 +159,7 @@ public function Field($properties = array()) /** - * Validates the captcha against the Recaptcha API + * Validates the captcha against the TurnstileCaptcha API * * @param Validator $validator Validator to send errors to * @return bool Returns boolean true if valid false if not @@ -172,71 +172,50 @@ public function validate($validator) if (!isset($recaptchaResponse)) { $validator->validationError($this->name, _t( - 'Terraformers\\TurnstileCaptcha\\Forms\\CaptchaField.EMPTY', + 'Terraformers\\TurnstileCaptcha\\Forms\\TurnstileCaptchaField.NOSCRIPT', 'if you do not see the captcha you must enable JavaScript'), 'validation'); return false; } - if (!function_exists('curl_init')) { - user_error('You must enable php-curl to use this field', E_USER_ERROR); - return false; - } - + $curlOptions = []; $secret_key = $this->_secretKey ?: self::config()->secret_key; - $url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'; - $ch = curl_init($url); $proxy_server = $this->_proxyServer ?: self::config()->proxy_server; if (!empty($proxy_server)) { - curl_setopt($ch, CURLOPT_PROXY, $proxy_server); + $curlOptions[CURLOPT_PROXY] = $proxy_server; $proxy_auth = $this->_proxyAuth ?: self::config()->proxy_auth; if (!empty($proxy_auth)) { - curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxy_auth); + $curlOptions[CURLOPT_PROXYUSERPWD] = $proxy_auth; } $proxy_port = $this->_proxyPort ?: self::config()->proxy_port; if (!empty($proxy_port)) { - curl_setopt($ch, CURLOPT_PROXYPORT, $proxy_port); + $curlOptions[CURLOPT_PROXYPORT] = $proxy_port; } } + $curlOptions[CURLOPT_RETURNTRANSFER] = true; + $curlOptions[CURLOPT_SSL_VERIFYPEER] = self::config()->verify_ssl; + $curlOptions[CURLOPT_USERAGENT] = str_replace(',', '/', 'SilverStripe'); + $client = HttpClient::singleton(); - curl_setopt($ch, CURLOPT_TIMEOUT, 10); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, self::config()->verify_ssl); - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_USERAGENT, str_replace(',', '/', 'SilverStripe')); - curl_setopt( - $ch, - CURLOPT_POSTFIELDS, - http_build_query([ + $response = $client->request('POST', 'https://challenges.cloudflare.com/turnstile/v0/siteverify', [ + 'json' => [ 'secret' => $secret_key, 'response' => $recaptchaResponse, 'remoteip' => $request->getIP(), - ]) - ); - $response = json_decode(curl_exec($ch), true); - - if (is_array($response)) { - $this->verifyResponse = $response; - - if (!array_key_exists('success', $response) || !$response['success']) { - $validator->validationError($this->name, _t( - 'Terraformers\\TurnstileCaptcha\\Forms\\CaptchaField.EMPTY', - '_Please answer the captcha, - if you do not see the captcha you must enable JavaScript'), - 'validation'); - return false; - } + ], + 'curl' => $curlOptions + ]); - } else { + if ($response->getStatusCode() !== 200) { $validator->validationError($this->name, _t( - 'Terraformers\\TurnstileCaptcha\\Forms\\CaptchaField.VALIDATE_ERROR', + 'Terraformers\\TurnstileCaptcha\\Forms\\TurnstileCaptchaField.VALIDATE_ERROR', '_Captcha could not be validated'), 'validation'); $logger = Injector::inst()->get(LoggerInterface::class); $logger->error( - 'Captcha validation failed as request was not successful.' + 'Turnstile Captch Field validation failed as request was not successful.' ); return false; } @@ -268,7 +247,7 @@ public function getHandleSubmitEvents(): bool /** * Sets the theme for this captcha - * @param string $value Theme to set it to, currently the api supports light and dark + * @param string $value Theme to set it to, currently the api supports light, dark & auto * @return TurnstileCaptchaField */ public function setTheme(string $value): TurnstileCaptchaField @@ -288,7 +267,7 @@ public function getCaptchaTheme(): string } /** - * Gets the site key configured via NocaptchaField.site_key this is used in the template + * Gets the site key configured via TurnstileCaptchaField.site_key this is used in the template * @return string */ public function getSiteKey(): string @@ -297,7 +276,7 @@ public function getSiteKey(): string } /** - * Setter for _siteKey to allow injector config to override the value + * Setter for _siteKey, this will override the injector or environment variable configuration */ public function setSiteKey($key) { @@ -305,7 +284,7 @@ public function setSiteKey($key) } /** - * Setter for _secretKey to allow injector config to override the value + * Setter for _secretKey, this will override the injector or environment variable configuration */ public function setSecretKey($key) { @@ -313,7 +292,7 @@ public function setSecretKey($key) } /** - * Setter for _proxyServer to allow injector config to override the value + * Setter for _proxyServer, this will override the injector or environment variable configuration */ public function setProxyServer($server) { @@ -321,7 +300,7 @@ public function setProxyServer($server) } /** - * Setter for _proxyAuth to allow injector config to override the value + * Setter for _proxyAuth, this will override the injector or environment variable configuration */ public function setProxyAuth($auth) { @@ -329,7 +308,7 @@ public function setProxyAuth($auth) } /** - * Setter for _proxyPort to allow injector config to override the value + * Setter for _proxyPort, this will override the injector or environment variable configuration */ public function setProxyPort($port) { diff --git a/src/Forms/TurnstileCaptchaProtector.php b/src/Forms/TurnstileCaptchaProtector.php index 469c507..819945d 100644 --- a/src/Forms/TurnstileCaptchaProtector.php +++ b/src/Forms/TurnstileCaptchaProtector.php @@ -13,7 +13,7 @@ class TurnstileCaptchaProtector implements SpamProtector * @param $value * @return TurnstileCaptchaField */ - public function getFormField($name = "Recaptcha2Field", $title = "Captcha", $value = null): TurnstileCaptchaField + public function getFormField($name = "TurnstileCaptchaField", $title = "Captcha", $value = null): TurnstileCaptchaField { return TurnstileCaptchaField::create($name, $title); } diff --git a/templates/README.md b/templates/README.md deleted file mode 100644 index f4aa8fc..0000000 --- a/templates/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Templates - -Any templates your module uses should be put in here. - -See the [templates and views documentation](https://docs.silverstripe.org/en/developer_guides/templates/) - -Make sure to remove this readme in your actual module! diff --git a/templates/Terraformers/TurnstileCaptcha/Forms/TurnstileCaptchaField.ss b/templates/Terraformers/TurnstileCaptcha/Forms/TurnstileCaptchaField.ss index 01e1c24..62eca6e 100644 --- a/templates/Terraformers/TurnstileCaptcha/Forms/TurnstileCaptchaField.ss +++ b/templates/Terraformers/TurnstileCaptcha/Forms/TurnstileCaptchaField.ss @@ -7,5 +7,5 @@ From c3388e91092a1a1d574f3615dea335c14fedebc3 Mon Sep 17 00:00:00 2001 From: "y2ksoft@gmail.com" Date: Wed, 28 Jun 2023 08:29:51 +1200 Subject: [PATCH 2/7] peer review changes --- src/Forms/TurnstileCaptchaField.php | 38 ++++++++++------ src/{Forms => Http}/HttpClient.php | 2 +- tests/README.md | 5 --- tests/TestTurnstileCaptchaField.php | 69 +++++++++++++++++++++++++++++ tests/endtoend/README.md | 5 --- tests/php/README.md | 5 --- 6 files changed, 94 insertions(+), 30 deletions(-) rename src/{Forms => Http}/HttpClient.php (89%) delete mode 100644 tests/README.md create mode 100644 tests/TestTurnstileCaptchaField.php delete mode 100644 tests/endtoend/README.md delete mode 100644 tests/php/README.md diff --git a/src/Forms/TurnstileCaptchaField.php b/src/Forms/TurnstileCaptchaField.php index 8b4fc29..4b6ebc7 100644 --- a/src/Forms/TurnstileCaptchaField.php +++ b/src/Forms/TurnstileCaptchaField.php @@ -2,13 +2,16 @@ namespace Terraformers\TurnstileCaptcha\Forms; +use GuzzleHttp\Exception\RequestException; +use Locale; use Psr\Log\LoggerInterface; use SilverStripe\Control\Controller; +use SilverStripe\Core\Environment; use SilverStripe\Core\Injector\Injector; use SilverStripe\Forms\FormField; use SilverStripe\i18n\i18n; use SilverStripe\View\Requirements; -use Locale; +use Terraformers\TurnstileCaptcha\Http\HttpClient; class TurnstileCaptchaField extends FormField { @@ -141,7 +144,7 @@ public function __construct($name, $title = null, $value = null) public function Field($properties = array()) { $siteKey = $this->getSiteKey(); - $secretKey = $this->_secretKey ? $this->_secretKey : self::config()->secret_key; + $secretKey = $this->_secretKey ? $this->_secretKey : Environment::getEnv('SS_TURNSTILE_SECRET_KEY'); if (empty($siteKey) || empty($secretKey)) { user_error('You must configure site_key and secret_key, you can retrieve these at https://developers.cloudflare.com/turnstile/', E_USER_ERROR); @@ -179,7 +182,7 @@ public function validate($validator) } $curlOptions = []; - $secret_key = $this->_secretKey ?: self::config()->secret_key; + $secret_key = $this->_secretKey ?: Environment::getEnv('SS_TURNSTILE_SECRET_KEY'); $proxy_server = $this->_proxyServer ?: self::config()->proxy_server; if (!empty($proxy_server)) { $curlOptions[CURLOPT_PROXY] = $proxy_server; @@ -197,16 +200,23 @@ public function validate($validator) $curlOptions[CURLOPT_RETURNTRANSFER] = true; $curlOptions[CURLOPT_SSL_VERIFYPEER] = self::config()->verify_ssl; $curlOptions[CURLOPT_USERAGENT] = str_replace(',', '/', 'SilverStripe'); - $client = HttpClient::singleton(); - - $response = $client->request('POST', 'https://challenges.cloudflare.com/turnstile/v0/siteverify', [ - 'json' => [ - 'secret' => $secret_key, - 'response' => $recaptchaResponse, - 'remoteip' => $request->getIP(), - ], - 'curl' => $curlOptions - ]); + $client = HttpClient::create()->getClient(); + + try { + $response = $client->request('POST', 'https://challenges.cloudflare.com/turnstile/v0/siteverify',[ + 'json' => [ + 'secret' => $secret_key, + 'response' => $recaptchaResponse, + 'remoteip' => $request->getIP(), + ], + 'curl' => $curlOptions, + 'timeout' =>10 + ]); + }catch (Throwable $e) { + if($e instanceof RequestException && $e->hasResponse()){ + return $e->getResponse(); + } + } if ($response->getStatusCode() !== 200) { $validator->validationError($this->name, _t( @@ -272,7 +282,7 @@ public function getCaptchaTheme(): string */ public function getSiteKey(): string { - return $this->_sitekey ? $this->_sitekey : self::config()->site_key; + return $this->_sitekey ? $this->_sitekey : Environment::getEnv('SS_TURNSTILE_SITE_KEY'); } /** diff --git a/src/Forms/HttpClient.php b/src/Http/HttpClient.php similarity index 89% rename from src/Forms/HttpClient.php rename to src/Http/HttpClient.php index 4fde4bd..697e438 100644 --- a/src/Forms/HttpClient.php +++ b/src/Http/HttpClient.php @@ -1,6 +1,6 @@ setSiteKey(Environment::getEnv('SS_TURNSTILE_SITE_KEY')); + $turnstileCaptchField->setSecretKey(Environment::getEnv('SS_TURNSTILE_SECRET_KEY')); + $turnstileCaptchField->setForm($form); + $this->assertNotNull($turnstileCaptchField->getSiteKey()); + $this->assertStringContainsString("Form_Form", $turnstileCaptchField->getFormID()); + } + + /** + * Testing the local Mock API data response for the TurnstileCaptchaField + * @return void + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function testTurnstileMockApi(): void + { + + // Mock Request + $request = new Request('POST', 'https://challenges.cloudflare.com/turnstile/v0/siteverify', + [], ''); + + // Create a Mock Handler with success response data + $mock = new MockHandler([ + new Response(200, [], '{ + "success": true, + "challenge_ts": "2022-02-28T15:14:30.096Z", + "hostname": "example.com", + "error-codes": [], + "action": "login", + "cdata": "sessionid-123456789" + }') + ]); + $handleStack = HandlerStack::create($mock); + $client = new Client(['handler' => $handleStack]); + // mock response data + $response = $client->send($request); + $responseData = json_decode($response->getBody(), true); + // validating the response data + $this->assertTrue($responseData['success']); + $this->assertEmpty($responseData['error-codes']); + } + +} diff --git a/tests/endtoend/README.md b/tests/endtoend/README.md deleted file mode 100644 index 8e25944..0000000 --- a/tests/endtoend/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# End-to-end tests - -The supported modules use behat for end-to-end testing, but other options such as Cypress also work well. - -Make sure to remove this readme in your actual module! diff --git a/tests/php/README.md b/tests/php/README.md deleted file mode 100644 index 946fcba..0000000 --- a/tests/php/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Unit and functional tests - -Look over the [unit testing](https://docs.silverstripe.org/en/developer_guides/testing/unit_testing/) and [functional testing](https://docs.silverstripe.org/en/developer_guides/testing/functional_testing/) documentation. - -Make sure to remove this readme in your actual module! From b00c5790ad0a797b71c662ddeefe61069b3249bc Mon Sep 17 00:00:00 2001 From: "y2ksoft@gmail.com" Date: Thu, 29 Jun 2023 12:40:20 +1200 Subject: [PATCH 3/7] peer review changes --- README.md | 30 +++-- src/Forms/TurnstileCaptchaField.php | 186 ++++++++-------------------- src/Http/HttpClient.php | 27 ++-- tests/TestTurnstileCaptchaField.php | 38 +++--- 4 files changed, 103 insertions(+), 178 deletions(-) diff --git a/README.md b/README.md index fb26499..2849bf4 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@ Adds a "spam protection" field to SilverStripe userforms using Cloudflare's * SilverStripe 5.x * [SilverStripe Spam Protection 4.x](https://github.com/silverstripe/silverstripe-spamprotection/) -* PHP CURL ## Installation ``` @@ -16,8 +15,8 @@ composer require silverstripe-terraformers/turnstile-captcha ``` After installing the module via composer or manual install you must set the spam -protector to NocaptchaProtector, this needs to be set in your site's config file -normally this is mysite/\_config/config.yml. +protector to TurnstileCaptchaProtector, this needs to be set in your site's config file +normally this is mysite/_config/config.yml. ```yml SilverStripe\SpamProtection\Extension\FormSpamProtectionExtension: default_spam_protector: Terraformers\TurnstileCaptcha\Forms\TurnstileCaptchaProtector @@ -31,24 +30,31 @@ $form->enableSpamProtection(); ## Configuration There are multiple configuration options for the field, you must set the -site_key and the secret_key which you can get from the [turnstile +site_key and the secret_key in the .env variable. +```yml +SS_TURNSTILE_SITE_KEY="" +SS_TURNSTILE_SECRET_KEY="" +``` + +You can get from the [turnstile page](https://developers.cloudflare.com/turnstile/). These configuration options must be -added to your site's yaml config typically this is mysite/\_config/config.yml. +added to your site's yaml config typically this is mysite/_config/config.yml. ```yml Terraformers\TurnstileCaptcha\Forms\TurnstileCaptchaField: - site_key: "YOUR_SITE_KEY" #Your site key (required) - secret_key: "YOUR_SECRET_KEY" #Your secret key (required) - verify_ssl: true #Allows you to disable php-curl's SSL peer verification by setting this to false (optional, defaults to true) default_theme: "light" #Default theme color (optional, light or dark, defaults to light) default_handle_submit: true #Default setting for whether nocaptcha should handle form submission. See "Handling form submission" below. - proxy_server: "" #Your proxy server address (optional) - proxy_port: "" #Your proxy server address port (optional) - proxy_auth: "" #Your proxy server authentication information (optional) +``` +TurnstileCaptchaField is using the HttpClient has a dependency , you can configure your own HttpClient class with the Curl options + +```yml +SilverStripe\Core\Injector\Injector: + Terraformers\TurnstileCaptcha\Http\HttpClient: + class: App\HttpClient ``` ## Adding field labels -If you want to add a field label or help text to the Captcha field you can do so +If you want to add a field label or help text to the TurnstileCaptchaField field you can do so like this: ```php diff --git a/src/Forms/TurnstileCaptchaField.php b/src/Forms/TurnstileCaptchaField.php index 4b6ebc7..222114c 100644 --- a/src/Forms/TurnstileCaptchaField.php +++ b/src/Forms/TurnstileCaptchaField.php @@ -2,8 +2,9 @@ namespace Terraformers\TurnstileCaptcha\Forms; -use GuzzleHttp\Exception\RequestException; +use GuzzleHttp\Exception\GuzzleException; use Locale; +use Psr\Container\NotFoundExceptionInterface; use Psr\Log\LoggerInterface; use SilverStripe\Control\Controller; use SilverStripe\Core\Environment; @@ -13,51 +14,29 @@ use SilverStripe\View\Requirements; use Terraformers\TurnstileCaptcha\Http\HttpClient; +/** + * @property HttpClient $httpClient + */ class TurnstileCaptchaField extends FormField { /** - * Recaptcha Site Key + * TurnstileCaptchaField Site Key * @config TurnstileCaptchaField.site_key */ private static ?string $site_key = null; /** - * Recaptcha Secret Key + * TurnstileCaptchaField Secret Key * @config TurnstileCaptchaField.secret_key */ private static ?string $secret_key = null; - /** - * CURL Proxy Server location - * @config TurnstileCaptchaField.proxy_server - */ - private static ?string $proxy_server = null; - - /** - * CURL Proxy authentication - * @config TurnstileCaptchaField.proxy_auth - */ - private static ?string $proxy_auth = null; - - /** - * CURL Proxy port - * @config TurnstileCaptchaField.proxy_port - */ - private static $proxy_port; - - /** - * Verify SSL Certificates - * @config TurnstileCaptchaField.verify_ssl - * @default true - */ - private static bool $verify_ssl = true; - /** * Captcha theme, currently options are light and dark * @default light */ - private static string $default_theme = 'light'; + private static string $default_theme = 'auto'; /** @@ -67,35 +46,6 @@ class TurnstileCaptchaField extends FormField */ private static bool $default_handle_submit = true; - /** - * TurnstileCaptcha Site Key - * Configurable via Injector config - */ - protected ?string $_siteKey = null; - - /** - * TurnstileCaptcha Site Key - * Configurable via Injector config - */ - protected ?string $_secretKey = null; - - /** - * CURL Proxy Server location - * Configurable via Injector config - */ - protected ?string $_proxyServer = null; - - /** - * CURL Proxy authentication - * Configurable via Injector config - */ - protected ?string $_proxyAuth = null; - - /** - * CURL Proxy port - * Configurable via Injector config - */ - protected $_proxyPort; /** * Onload callback to be called for Turnstile is loaded @@ -120,6 +70,10 @@ class TurnstileCaptchaField extends FormField */ private bool $handleSubmitEvents; + private static array $dependencies = [ + 'httpClient' => '%$' . HttpClient::class + ]; + /** * Creates a new TurnstileCaptcha 2 field. * @param string $name The internal field name, passed to forms. @@ -144,12 +98,13 @@ public function __construct($name, $title = null, $value = null) public function Field($properties = array()) { $siteKey = $this->getSiteKey(); - $secretKey = $this->_secretKey ? $this->_secretKey : Environment::getEnv('SS_TURNSTILE_SECRET_KEY'); + $secretKey = $this->getSecretKey(); if (empty($siteKey) || empty($secretKey)) { user_error('You must configure site_key and secret_key, you can retrieve these at https://developers.cloudflare.com/turnstile/', E_USER_ERROR); } + Requirements::javascript( 'https://challenges.cloudflare.com/turnstile/v0/api.js?hl=' . Locale::getPrimaryLanguage(i18n::get_locale()) . ($this->config()->js_onload_callback ? '&onload=' . $this->config()->js_onload_callback : ''), [ @@ -165,15 +120,16 @@ public function Field($properties = array()) * Validates the captcha against the TurnstileCaptcha API * * @param Validator $validator Validator to send errors to - * @return bool Returns boolean true if valid false if not + * @return bool Returns boolean + * @throws NotFoundExceptionInterface */ - public function validate($validator) + public function validate($validator): bool { $request = Controller::curr()->getRequest(); - $recaptchaResponse = $request->requestVar('cf-turnstile-response'); + $captchaResponse = $request->requestVar('cf-turnstile-response'); - if (!isset($recaptchaResponse)) { + if (!isset($captchaResponse)) { $validator->validationError($this->name, _t( 'Terraformers\\TurnstileCaptcha\\Forms\\TurnstileCaptchaField.NOSCRIPT', 'if you do not see the captcha you must enable JavaScript'), @@ -181,51 +137,36 @@ public function validate($validator) return false; } - $curlOptions = []; - $secret_key = $this->_secretKey ?: Environment::getEnv('SS_TURNSTILE_SECRET_KEY'); - $proxy_server = $this->_proxyServer ?: self::config()->proxy_server; - if (!empty($proxy_server)) { - $curlOptions[CURLOPT_PROXY] = $proxy_server; - - $proxy_auth = $this->_proxyAuth ?: self::config()->proxy_auth; - if (!empty($proxy_auth)) { - $curlOptions[CURLOPT_PROXYUSERPWD] = $proxy_auth; - } - - $proxy_port = $this->_proxyPort ?: self::config()->proxy_port; - if (!empty($proxy_port)) { - $curlOptions[CURLOPT_PROXYPORT] = $proxy_port; - } + + $client = $this->httpClient->getClient(); + + try { + $response = $client->request('POST', 'https://challenges.cloudflare.com/turnstile/v0/siteverify', [ + 'json' => [ + 'secret' => $this->getSecretKey(), + 'response' => $captchaResponse, + 'remoteip' => $request->getIP(), + ], + 'timeout' => 10 + ]); + } catch (GuzzleException $e) { + $logger = Injector::inst()->get(LoggerInterface::class); + $logger->error($e->getMessage()); + return false; + } + $responseBody = json_decode($response->getBody(), true); + + if (is_array($responseBody)) { + $this->verifyResponse = $responseBody; } - $curlOptions[CURLOPT_RETURNTRANSFER] = true; - $curlOptions[CURLOPT_SSL_VERIFYPEER] = self::config()->verify_ssl; - $curlOptions[CURLOPT_USERAGENT] = str_replace(',', '/', 'SilverStripe'); - $client = HttpClient::create()->getClient(); - - try { - $response = $client->request('POST', 'https://challenges.cloudflare.com/turnstile/v0/siteverify',[ - 'json' => [ - 'secret' => $secret_key, - 'response' => $recaptchaResponse, - 'remoteip' => $request->getIP(), - ], - 'curl' => $curlOptions, - 'timeout' =>10 - ]); - }catch (Throwable $e) { - if($e instanceof RequestException && $e->hasResponse()){ - return $e->getResponse(); - } - } - - if ($response->getStatusCode() !== 200) { + if ($response->getStatusCode() !== 200 || !$this->verifyResponse['success']) { $validator->validationError($this->name, _t( 'Terraformers\\TurnstileCaptcha\\Forms\\TurnstileCaptchaField.VALIDATE_ERROR', - '_Captcha could not be validated'), + 'Turnstile Captcha Field could not be validated'), 'validation'); $logger = Injector::inst()->get(LoggerInterface::class); $logger->error( - 'Turnstile Captch Field validation failed as request was not successful.' + 'Turnstile Captcha Field validation failed as request was not successful.' ); return false; } @@ -277,53 +218,24 @@ public function getCaptchaTheme(): string } /** - * Gets the site key configured via TurnstileCaptchaField.site_key this is used in the template + * Gets the site key configured via .env variable this is used in the template * @return string */ public function getSiteKey(): string { - return $this->_sitekey ? $this->_sitekey : Environment::getEnv('SS_TURNSTILE_SITE_KEY'); - } - /** - * Setter for _siteKey, this will override the injector or environment variable configuration - */ - public function setSiteKey($key) - { - $this->_sitekey = $key; + return Environment::getEnv('SS_TURNSTILE_SITE_KEY'); } /** - * Setter for _secretKey, this will override the injector or environment variable configuration - */ - public function setSecretKey($key) - { - $this->_secretKey = $key; - } - - /** - * Setter for _proxyServer, this will override the injector or environment variable configuration - */ - public function setProxyServer($server) - { - $this->_proxyServer = $server; - } - - /** - * Setter for _proxyAuth, this will override the injector or environment variable configuration + * Gets the site key configured via .env variable this is used in the template + * @return string */ - public function setProxyAuth($auth) + public function getSecretKey(): string { - $this->_proxyAuth = $auth; + return Environment::getEnv('SS_TURNSTILE_SECRET_KEY'); } - /** - * Setter for _proxyPort, this will override the injector or environment variable configuration - */ - public function setProxyPort($port) - { - $this->_proxyPort = $port; - } /** * Gets the form's id diff --git a/src/Http/HttpClient.php b/src/Http/HttpClient.php index 697e438..54fa863 100644 --- a/src/Http/HttpClient.php +++ b/src/Http/HttpClient.php @@ -7,20 +7,21 @@ class HttpClient { - use Injectable; + use Injectable; - protected ?Client $client; - public function __construct(?Client $client = null) - { - if($client === null) { - $client = new Client(); - } + protected ?Client $client; - $this->client = $client; - } + public function __construct(?Client $client = null) + { + if ($client === null) { + $client = new Client(); + } - public function getClient(): Client - { - return $this->client; - } + $this->client = $client; + } + + public function getClient(): Client + { + return $this->client; + } } diff --git a/tests/TestTurnstileCaptchaField.php b/tests/TestTurnstileCaptchaField.php index 47a4867..cdc722f 100644 --- a/tests/TestTurnstileCaptchaField.php +++ b/tests/TestTurnstileCaptchaField.php @@ -6,9 +6,7 @@ use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; use GuzzleHttp\Psr7\Response; -use GuzzleHttp\Psr7\Request; use SilverStripe\Control\Controller; -use SilverStripe\Core\Environment; use SilverStripe\Dev\SapphireTest; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\Form; @@ -26,8 +24,8 @@ public function testSpamProtectionField(): void $form = Form::create(Controller::create(), 'Form', new FieldList(), new FieldList()); $turnstileCaptchField = new TurnstileCaptchaField('turnstileField'); - $turnstileCaptchField->setSiteKey(Environment::getEnv('SS_TURNSTILE_SITE_KEY')); - $turnstileCaptchField->setSecretKey(Environment::getEnv('SS_TURNSTILE_SECRET_KEY')); + $turnstileCaptchField->setSiteKey('1x00000000000000000000AA'); + $turnstileCaptchField->setSecretKey('1x0000000000000000000000000000000AA'); $turnstileCaptchField->setForm($form); $this->assertNotNull($turnstileCaptchField->getSiteKey()); $this->assertStringContainsString("Form_Form", $turnstileCaptchField->getFormID()); @@ -41,11 +39,7 @@ public function testSpamProtectionField(): void public function testTurnstileMockApi(): void { - // Mock Request - $request = new Request('POST', 'https://challenges.cloudflare.com/turnstile/v0/siteverify', - [], ''); - - // Create a Mock Handler with success response data + // Create a Mock Handler with success & fail response data $mock = new MockHandler([ new Response(200, [], '{ "success": true, @@ -54,16 +48,28 @@ public function testTurnstileMockApi(): void "error-codes": [], "action": "login", "cdata": "sessionid-123456789" - }') + }'), + new Response(201, [], '{ + "success": false, + "error-codes": ["invalid-input-response"] + }'), ]); $handleStack = HandlerStack::create($mock); $client = new Client(['handler' => $handleStack]); - // mock response data - $response = $client->send($request); - $responseData = json_decode($response->getBody(), true); - // validating the response data - $this->assertTrue($responseData['success']); - $this->assertEmpty($responseData['error-codes']); + + // mock the success response data + $successResponse = $client->request('GET', '/'); + $successResponseBody = json_decode($successResponse->getBody(), true); + $this->assertTrue($successResponseBody['success']); + // expecting a empty error-code array + $this->assertEmpty($successResponseBody['error-codes']); + + // mock the failed response data + $failedResponse = $client->request('GET', '/'); + $failedResponseBody = json_decode($failedResponse->getBody(), true); + $this->assertFalse($failedResponseBody['success']); + // expecting a error message for a failed reponse + $this->assertNotEmpty($failedResponseBody['error-codes']); } } From 5916e242264f61a4c6f8e83d3a928aa7b51c8123 Mon Sep 17 00:00:00 2001 From: Bernie Hamlin Date: Fri, 30 Jun 2023 10:27:09 +1200 Subject: [PATCH 4/7] Cleanup docs --- README.md | 77 +++----------- src/Forms/TurnstileCaptchaField.php | 130 ++++++++++++------------ src/Forms/TurnstileCaptchaProtector.php | 8 +- src/Http/ClientInterface.php | 10 ++ src/Http/HttpClient.php | 2 +- src/readme.md | 5 - 6 files changed, 97 insertions(+), 135 deletions(-) create mode 100644 src/Http/ClientInterface.php delete mode 100644 src/readme.md diff --git a/README.md b/README.md index 2849bf4..0b7b627 100644 --- a/README.md +++ b/README.md @@ -29,22 +29,23 @@ $form->enableSpamProtection(); ``` ## Configuration -There are multiple configuration options for the field, you must set the -site_key and the secret_key in the .env variable. +Set the `site_key` and the `secret_key` via [environment variables](https://docs.silverstripe.org/en/5/getting_started/environment_management/). + ```yml SS_TURNSTILE_SITE_KEY="" SS_TURNSTILE_SECRET_KEY="" ``` -You can get from the [turnstile -page](https://developers.cloudflare.com/turnstile/). These configuration options must be -added to your site's yaml config typically this is mysite/_config/config.yml. +You can get these from your cloudflare account [refer to the turnstile documentation](https://developers.cloudflare.com/turnstile/). + +There are some optional configuration settings that can be +added to your site's yaml config (typically this is mysite/_config/config.yml). ```yml Terraformers\TurnstileCaptcha\Forms\TurnstileCaptchaField: - default_theme: "light" #Default theme color (optional, light or dark, defaults to light) - default_handle_submit: true #Default setting for whether nocaptcha should handle form submission. See "Handling form submission" below. + default_theme: "light" #Default theme color (optional, light or dark, defaults to auto) + default_render_type: 'explicit' #Default setting for how to render the widget. See the "Render Type" section below. ``` -TurnstileCaptchaField is using the HttpClient has a dependency , you can configure your own HttpClient class with the Curl options +TurnstileCaptchaField uses Guzzle to communicate with cloudflare. If you would like to change http connection settings (Eg proxy settings) you can configure your own HttpClient class via injector ```yml SilverStripe\Core\Injector\Injector: @@ -59,7 +60,7 @@ like this: ```php $form->enableSpamProtection() - ->fields()->fieldByName('Captcha') + ->fields()->fieldByName('TurnstileCaptchaField') ->setTitle("Spam protection") ->setDescription("Please tick the box to prove you're a human and help us stop spam."); ``` @@ -74,68 +75,22 @@ in order to use Terraformers\TurnstileCaptcha on comment forms. CommentingController::add_extension('CommentSpamProtection'); ``` -## Retrieving the Verify Response - -If you wish to manually retrieve the Site Verify response in you form action use -the `getVerifyResponse()` method - -```php -function doSubmit($data, $form) { - $captchaResponse = $form->Fields()->fieldByName('Captcha')->getVerifyResponse(); - - // $captchaResponse = array (size=5) [ - // 'success' => boolean true - // 'challenge_ts' => string '2020-09-08T20:48:34Z' (length=20) - // 'hostname' => string 'localhost' (length=9) - // 'score' => float 0.9 - // 'action' => string 'submit' (length=6) - // ]; -} -``` - -## Handling form submission -By default, the javascript included with this module will add a submit event handler to your form. - -If you need to handle form submissions in a special way (for example to support front-end validation), -you can choose to handle form submit events yourself. +## Render type +By default, the turnstyle widget will be rendered automatically. To change this you can set the render type. This can be configured site-wide using the Config API ```yml Terraformers\TurnstileCaptcha\Forms\TurnstileCaptchaField: - default_handle_submit: false + default_render_type: 'explicit' ``` Or on a per form basis: ```php -$captchaField = $form->Fields()->fieldByName('Captcha'); -$captchaField->setHandleSubmitEvents(false); +$captchaField = $form->Fields()->fieldByName('TurnstileCaptchaField'); +$captchaField->setRenderType('explicit'); ``` -With this configuration no event handlers will be added by this module to your form. Instead, a -function will be provided called `nocaptcha_handleCaptcha` which you can call from your code -when you're ready to submit your form. It has the following signature: -```js -function nocaptcha_handleCaptcha(form, callback) -``` -`form` must be the form element, and `callback` should be a function that finally submits the form, -though it is optional. - -In the simplest case, you can use it like this: -```js -document.addEventListener("DOMContentLoaded", function(event) { - // where formID is the element ID for your form - const form = document.getElementById(formID); - const submitListener = function(event) { - event.preventDefault(); - let valid = true; - /* Your validation logic here */ - if (valid) { - nocaptcha_handleCaptcha(form, form.submit.bind(form)); - } - }; - form.addEventListener('submit', submitListener); -}); -``` +With this configuration you will need to add your own javascript to render the widget. Refer to the [cloudflare documentation](https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#explicitly-render-the-turnstile-widget) for details. ## Reporting an issue diff --git a/src/Forms/TurnstileCaptchaField.php b/src/Forms/TurnstileCaptchaField.php index 222114c..cef38b1 100644 --- a/src/Forms/TurnstileCaptchaField.php +++ b/src/Forms/TurnstileCaptchaField.php @@ -10,42 +10,26 @@ use SilverStripe\Core\Environment; use SilverStripe\Core\Injector\Injector; use SilverStripe\Forms\FormField; +use SilverStripe\Forms\Validator; use SilverStripe\i18n\i18n; use SilverStripe\View\Requirements; +use Terraformers\TurnstileCaptcha\Http\ClientInterface; use Terraformers\TurnstileCaptcha\Http\HttpClient; -/** - * @property HttpClient $httpClient - */ class TurnstileCaptchaField extends FormField { - /** - * TurnstileCaptchaField Site Key - * @config TurnstileCaptchaField.site_key - */ - private static ?string $site_key = null; - - /** - * TurnstileCaptchaField Secret Key - * @config TurnstileCaptchaField.secret_key - */ - private static ?string $secret_key = null; - - /** * Captcha theme, currently options are light and dark * @default light */ private static string $default_theme = 'auto'; - /** - * Whether form submit events are handled directly by this module. - * If false, a function is provided that can be called by user code submit handlers. - * @default true + * control the turnstile render mode + * options are implicit or explicit + * @defautl implicit */ - private static bool $default_handle_submit = true; - + private static bool $default_render_type = 'implicit'; /** * Onload callback to be called for Turnstile is loaded @@ -56,26 +40,27 @@ class TurnstileCaptchaField extends FormField /** * Captcha theme, currently options are light and dark */ - private ?string $_captchaTheme = null; + private ?string $captchaTheme = null; /** * The verification response */ - protected array $verifyResponse; + protected array $verifyResponse = ['success' => false]; - - /** - * Whether form submit events are handled directly by this module. - * If false, a function is provided that can be called by user code submit handlers. - */ - private bool $handleSubmitEvents; + private bool $renderType; private static array $dependencies = [ 'httpClient' => '%$' . HttpClient::class ]; /** - * Creates a new TurnstileCaptcha 2 field. + * HTTP client object + * + */ + public ClientInterface $httpClient; + + /** + * Creates a new TurnstileCaptcha field. * @param string $name The internal field name, passed to forms. * @param string $title The human-readable field label. * @param mixed $value The value of the field (unused) @@ -86,8 +71,8 @@ public function __construct($name, $title = null, $value = null) $this->title = $title; - $this->_captchaTheme = self::config()->default_theme; - $this->handleSubmitEvents = self::config()->default_handle_submit; + $this->captchaTheme = self::config()->default_theme; + $this->setRenderType(self::config()->default_render_type); } /** @@ -95,7 +80,7 @@ public function __construct($name, $title = null, $value = null) * @param array $properties Array of properties for the form element (not used) * @return string Rendered field template */ - public function Field($properties = array()) + public function Field($properties = array()) //phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps { $siteKey = $this->getSiteKey(); $secretKey = $this->getSecretKey(); @@ -106,7 +91,10 @@ public function Field($properties = array()) Requirements::javascript( - 'https://challenges.cloudflare.com/turnstile/v0/api.js?hl=' . Locale::getPrimaryLanguage(i18n::get_locale()) . ($this->config()->js_onload_callback ? '&onload=' . $this->config()->js_onload_callback : ''), + 'https://challenges.cloudflare.com/turnstile/v0/api.js?language=' + . Locale::getPrimaryLanguage(i18n::get_locale()) + . ($this->config()->js_onload_callback ? '&onload=' . $this->config()->js_onload_callback : '') + . ($this->getRenderType() === 'explicit' ? '&render=explicit' : ''), [ 'async' => true, 'defer' => true, @@ -115,7 +103,6 @@ public function Field($properties = array()) return parent::Field($properties); } - /** * Validates the captcha against the TurnstileCaptcha API * @@ -130,10 +117,14 @@ public function validate($validator): bool $captchaResponse = $request->requestVar('cf-turnstile-response'); if (!isset($captchaResponse)) { - $validator->validationError($this->name, _t( - 'Terraformers\\TurnstileCaptcha\\Forms\\TurnstileCaptchaField.NOSCRIPT', - 'if you do not see the captcha you must enable JavaScript'), - 'validation'); + $validator->validationError( + $this->name, + _t( + 'Terraformers\\TurnstileCaptcha\\Forms\\TurnstileCaptchaField.NOSCRIPT', + 'if you do not see the captcha you must enable JavaScript' + ), + 'validation' + ); return false; } @@ -141,29 +132,37 @@ public function validate($validator): bool $client = $this->httpClient->getClient(); try { - $response = $client->request('POST', 'https://challenges.cloudflare.com/turnstile/v0/siteverify', [ - 'json' => [ - 'secret' => $this->getSecretKey(), - 'response' => $captchaResponse, - 'remoteip' => $request->getIP(), - ], - 'timeout' => 10 - ]); + $response = $client->request( + 'POST', + 'https://challenges.cloudflare.com/turnstile/v0/siteverify', + [ + 'json' => [ + 'secret' => $this->getSecretKey(), + 'response' => $captchaResponse, + 'remoteip' => $request->getIP(), + ] + ] + ); + + $responseBody = json_decode($response->getBody(), true); + if (is_array($responseBody)) { + $this->verifyResponse = $responseBody; + } } catch (GuzzleException $e) { $logger = Injector::inst()->get(LoggerInterface::class); $logger->error($e->getMessage()); return false; } - $responseBody = json_decode($response->getBody(), true); - if (is_array($responseBody)) { - $this->verifyResponse = $responseBody; - } if ($response->getStatusCode() !== 200 || !$this->verifyResponse['success']) { - $validator->validationError($this->name, _t( - 'Terraformers\\TurnstileCaptcha\\Forms\\TurnstileCaptchaField.VALIDATE_ERROR', - 'Turnstile Captcha Field could not be validated'), - 'validation'); + $validator->validationError( + $this->name, + _t( + 'Terraformers\\TurnstileCaptcha\\Forms\\TurnstileCaptchaField.VALIDATE_ERROR', + 'Turnstile Captcha Field could not be validated' + ), + 'validation' + ); $logger = Injector::inst()->get(LoggerInterface::class); $logger->error( 'Turnstile Captcha Field validation failed as request was not successful.' @@ -175,25 +174,25 @@ public function validate($validator): bool } /** - * Sets whether form submit events are handled directly by this module. + * Sets render type of the turnstyle widget * * @param boolean $value * @return TurnstileCaptchaField */ - public function setHandleSubmitEvents(bool $value): TurnstileCaptchaField + public function setRenderType(bool $value): TurnstileCaptchaField { - $this->handleSubmitEvents = $value; + $this->renderType = $value; return $this; } /** - * Get whether form submit events are handled directly by this module. + * Get the render type of the turnstyle widget * * @return boolean */ - public function getHandleSubmitEvents(): bool + public function getRenderType(): bool { - return $this->handleSubmitEvents; + return $this->renderType; } /** @@ -203,7 +202,7 @@ public function getHandleSubmitEvents(): bool */ public function setTheme(string $value): TurnstileCaptchaField { - $this->_captchaTheme = $value; + $this->captchaTheme = $value; return $this; } @@ -214,7 +213,7 @@ public function setTheme(string $value): TurnstileCaptchaField */ public function getCaptchaTheme(): string { - return $this->_captchaTheme; + return $this->captchaTheme; } /** @@ -247,11 +246,12 @@ public function getFormID(): ?string } /** + * get response object + * used in tests * @return array */ public function getVerifyResponse(): array { return $this->verifyResponse; } - } diff --git a/src/Forms/TurnstileCaptchaProtector.php b/src/Forms/TurnstileCaptchaProtector.php index 819945d..d3c0307 100644 --- a/src/Forms/TurnstileCaptchaProtector.php +++ b/src/Forms/TurnstileCaptchaProtector.php @@ -6,15 +6,17 @@ class TurnstileCaptchaProtector implements SpamProtector { - /** * @param $name * @param $title * @param $value * @return TurnstileCaptchaField */ - public function getFormField($name = "TurnstileCaptchaField", $title = "Captcha", $value = null): TurnstileCaptchaField - { + public function getFormField( + $name = "TurnstileCaptchaField", + $title = "Captcha", + $value = null + ): TurnstileCaptchaField { return TurnstileCaptchaField::create($name, $title); } diff --git a/src/Http/ClientInterface.php b/src/Http/ClientInterface.php new file mode 100644 index 0000000..db31445 --- /dev/null +++ b/src/Http/ClientInterface.php @@ -0,0 +1,10 @@ + Date: Fri, 30 Jun 2023 12:13:09 +1200 Subject: [PATCH 5/7] test field validate --- .gitignore | 4 + composer.json | 7 +- phpunit.xml.dist | 4 +- src/Forms/TurnstileCaptchaField.php | 10 ++- tests/TestTurnstileCaptchaField.php | 75 ----------------- tests/TurnstileCaptchaFieldTest.php | 124 ++++++++++++++++++++++++++++ 6 files changed, 145 insertions(+), 79 deletions(-) create mode 100644 .gitignore delete mode 100644 tests/TestTurnstileCaptchaField.php create mode 100644 tests/TurnstileCaptchaFieldTest.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..faf7d83 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.vscode +composer.lock +public +vendor diff --git a/composer.json b/composer.json index cdfb48b..5523473 100644 --- a/composer.json +++ b/composer.json @@ -17,12 +17,17 @@ ], "require": { "php": "^8.1", + "php-intl": "*", "silverstripe/framework": "^5.0", "silverstripe/spamprotection": "^4" }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, "autoload": { "psr-4": { - "Terraformers\\TurnstileCaptcha\\": "src/" + "Terraformers\\TurnstileCaptcha\\": "src/", + "Terraformers\\TurnstileCaptcha\\Test\\": "tests/" } }, "extra": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 3801dc3..ec8e43b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,8 +1,8 @@ - + - tests/php + tests diff --git a/src/Forms/TurnstileCaptchaField.php b/src/Forms/TurnstileCaptchaField.php index cef38b1..8fefe78 100644 --- a/src/Forms/TurnstileCaptchaField.php +++ b/src/Forms/TurnstileCaptchaField.php @@ -29,7 +29,7 @@ class TurnstileCaptchaField extends FormField * options are implicit or explicit * @defautl implicit */ - private static bool $default_render_type = 'implicit'; + private static string $default_render_type = 'implicit'; /** * Onload callback to be called for Turnstile is loaded @@ -151,6 +151,14 @@ public function validate($validator): bool } catch (GuzzleException $e) { $logger = Injector::inst()->get(LoggerInterface::class); $logger->error($e->getMessage()); + $validator->validationError( + $this->name, + _t( + 'Terraformers\\TurnstileCaptcha\\Forms\\TurnstileCaptchaField.VALIDATE_ERROR', + 'Turnstile Captcha Field could not be validated' + ), + 'validation' + ); return false; } diff --git a/tests/TestTurnstileCaptchaField.php b/tests/TestTurnstileCaptchaField.php deleted file mode 100644 index cdc722f..0000000 --- a/tests/TestTurnstileCaptchaField.php +++ /dev/null @@ -1,75 +0,0 @@ -setSiteKey('1x00000000000000000000AA'); - $turnstileCaptchField->setSecretKey('1x0000000000000000000000000000000AA'); - $turnstileCaptchField->setForm($form); - $this->assertNotNull($turnstileCaptchField->getSiteKey()); - $this->assertStringContainsString("Form_Form", $turnstileCaptchField->getFormID()); - } - - /** - * Testing the local Mock API data response for the TurnstileCaptchaField - * @return void - * @throws \GuzzleHttp\Exception\GuzzleException - */ - public function testTurnstileMockApi(): void - { - - // Create a Mock Handler with success & fail response data - $mock = new MockHandler([ - new Response(200, [], '{ - "success": true, - "challenge_ts": "2022-02-28T15:14:30.096Z", - "hostname": "example.com", - "error-codes": [], - "action": "login", - "cdata": "sessionid-123456789" - }'), - new Response(201, [], '{ - "success": false, - "error-codes": ["invalid-input-response"] - }'), - ]); - $handleStack = HandlerStack::create($mock); - $client = new Client(['handler' => $handleStack]); - - // mock the success response data - $successResponse = $client->request('GET', '/'); - $successResponseBody = json_decode($successResponse->getBody(), true); - $this->assertTrue($successResponseBody['success']); - // expecting a empty error-code array - $this->assertEmpty($successResponseBody['error-codes']); - - // mock the failed response data - $failedResponse = $client->request('GET', '/'); - $failedResponseBody = json_decode($failedResponse->getBody(), true); - $this->assertFalse($failedResponseBody['success']); - // expecting a error message for a failed reponse - $this->assertNotEmpty($failedResponseBody['error-codes']); - } - -} diff --git a/tests/TurnstileCaptchaFieldTest.php b/tests/TurnstileCaptchaFieldTest.php new file mode 100644 index 0000000..8d5d0ab --- /dev/null +++ b/tests/TurnstileCaptchaFieldTest.php @@ -0,0 +1,124 @@ +getStates(); + unset($states['fixtures']); + static::$state->setStates($states); + + static::$tempDB = null; + + } + + /** + * Testing the TurnstileCaptchaField Spam Protection Field + * @return void + */ + public function testSpamProtectionField(): void + { + $form = Form::create(Controller::create(), 'Form', new FieldList(), new FieldList()); + + $turnstileCaptchField = new TurnstileCaptchaField('turnstileField'); + Environment::setEnv('SS_TURNSTILE_SITE_KEY', '1x00000000000000000000AA'); + Environment::setEnv('SS_TURNSTILE_SECRET_KEY', '1x0000000000000000000000000000000AA'); + $turnstileCaptchField->setForm($form); + $this->assertNotNull($turnstileCaptchField->getSiteKey()); + $this->assertStringContainsString("Form_Form", $turnstileCaptchField->getFormID()); + } + + /** + * Testing the local Mock API data response for the TurnstileCaptchaField + * @return void + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function testTurnstileMockApi(): void + { + + $testClient = new class extends HttpClient + { + public function __construct(?Client $client = null) + { + + // Create a Mock Handler with success & fail response data + $mock = new MockHandler([ + // first request passes + new Response(200, [], '{ + "success": true, + "challenge_ts": "2022-02-28T15:14:30.096Z", + "hostname": "example.com", + "error-codes": [], + "action": "login", + "cdata": "sessionid-123456789" + }'), + // second fails + new Response(201, [], '{ + "success": false, + "error-codes": ["invalid-input-response"] + }'), + // third is an error + new Response(500, []), + ]); + $handleStack = HandlerStack::create($mock); + $client = new Client(['handler' => $handleStack]); + + $this->client = $client; + } + + }; + + Injector::inst()->registerService($testClient, HttpClient::class); + + $request = new HTTPRequest('POST', '/', [], ['cf-turnstile-response'=> 'abcd']); + Controller::curr()->setRequest($request); + + $turnstileCaptchField = TurnstileCaptchaField::create('turnstileField'); + $validator = RequiredFields::create(); + $validation = $turnstileCaptchField->validate($validator); + // first request should pass validation + $this->assertTrue($validation); + $this->assertEmpty($validator->getErrors()); + + // second should fail + $validator = RequiredFields::create(); + $validation = $turnstileCaptchField->validate($validator); + $this->assertFalse($validation); + $errors = $validator->getErrors(); + + $this->assertEquals('Captcha could not be validated', $errors[0]['message']); + + // third should fail gracefully (error) + $validator = RequiredFields::create(); + $validation = $turnstileCaptchField->validate($validator); + $this->assertFalse($validation); + $errors = $validator->getErrors(); + + $this->assertEquals('Captcha could not be validated', $errors[0]['message']); + } + +} From 2fb0ad75e0c0048b683cdcdd9c28ad13fe71747a Mon Sep 17 00:00:00 2001 From: Bernie Hamlin Date: Fri, 30 Jun 2023 13:38:12 +1200 Subject: [PATCH 6/7] correct extension syntax --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5523473..041a653 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ ], "require": { "php": "^8.1", - "php-intl": "*", + "ext-intl": "*", "silverstripe/framework": "^5.0", "silverstripe/spamprotection": "^4" }, From e39452a56693b48ae9af327be916fa327e33a1e2 Mon Sep 17 00:00:00 2001 From: Bernie Hamlin Date: Tue, 11 Jul 2023 13:30:59 +1200 Subject: [PATCH 7/7] remove WIP comment --- README.md | 5 ++--- src/Forms/TurnstileCaptchaField.php | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0b7b627..474ac25 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ -Turnstile Smart CAPTCHA +Silverstripe Turnstile CAPTCHA ================= -(Please note this is a initial work, Have not done any Test yet not ideal for production environment) Adds a "spam protection" field to SilverStripe userforms using Cloudflare's -[smart CAPTCHA](https://developers.cloudflare.com/turnstile) service. +[Turnstile CAPTCHA](https://developers.cloudflare.com/turnstile) service. ## Requirements * SilverStripe 5.x diff --git a/src/Forms/TurnstileCaptchaField.php b/src/Forms/TurnstileCaptchaField.php index 8fefe78..cb7a52b 100644 --- a/src/Forms/TurnstileCaptchaField.php +++ b/src/Forms/TurnstileCaptchaField.php @@ -97,7 +97,6 @@ public function Field($properties = array()) //phpcs:ignore PSR1.Methods.CamelCa . ($this->getRenderType() === 'explicit' ? '&render=explicit' : ''), [ 'async' => true, - 'defer' => true, ] ); return parent::Field($properties);