A comprehensive Nette Forms extension for integrating Cloudflare Turnstile and hCaptcha with your Nette applications.
- β Cloudflare Turnstile integration (visible, managed, invisible modes)
- β hCaptcha integration (visible, invisible modes)
- β PHP 8.3+ support
- β Server-side validation
- β Flexible configuration
- β Easy form integration
- β Customizable themes and sizes
- PHP: 8.3+
- Nette Forms: 3.2+
- Extensions:
curl(used by default) orallow_url_fopenfor server-side verification
Install via Composer:
composer require drabek-digital/captchaRegister the extension in your config.neon:
extensions:
captcha: DrabekDigital\Captcha\DI\CaptchaExtension
captcha:
type: turnstile # or hcaptcha
secretKey: your-secret-key
siteKey: your-site-key
theme: auto # light, dark, auto (optional)
size: normal # normal, compact, flexible (Turnstile only) (optional)| Option | Type | Required | Default | Description |
|---|---|---|---|---|
type |
string | No | turnstile |
Captcha provider (turnstile or hcaptcha) |
secretKey |
string | Yes | - | Your secret key from the captcha provider |
siteKey |
string | Yes | - | Your site key from the captcha provider |
verifyUrl |
string | No | - | Custom verification URL (uses default if not set) |
theme |
string | No | auto |
Theme: light, dark, or auto (hCaptcha does not support auto and uses its default light theme) |
size |
string | No | normal |
Size: normal, compact or flexible (Turnstile only) |
First do not forget to render relevant JS script include in your Latte templates. With Latte 3 and nette/application the extension registers a tag that renders the provider script for the configured type together with the validation script (including the CSP nonce, when used):
{captchaScripts}Without it, include the scripts manually:
{* This will include Turnstile or hCaptcha JS code *}
{DrabekDigital\Captcha\Assets\CaptchaFrontend::getJavascriptStatic(\DrabekDigital\Captcha\Enums\CaptchaType::TURNSTILE)|noescape} {* <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> *}
{DrabekDigital\Captcha\Assets\CaptchaFrontend::getJavascriptStatic(\DrabekDigital\Captcha\Enums\CaptchaType::HCAPTCHA)|noescape} {* <script src="https://js.hcaptcha.com/1/api.js" async defer></script> *}
{* Include local JS to make forms validation (prevent submission when verification is missing) and to show managed states labels *}
{* ... or manually link <LIBRARY PATH>/src/Assets/captcha-validation.js *}
{DrabekDigital\Captcha\Assets\CaptchaFrontend::getLocalJavascriptStatic()|noescape}use Nette\Application\UI\Form;
$form = new Form;
// Simple captcha (required by default)
// The validator is automatically injected by the DI extension
$form->addCaptcha('captcha', 'Verify you are human');
// Handle form submission
$form->onSuccess[] = function($form) {
$values = $form->getValues();
// Process form...
};// All supported method signatures:
// 1. Simple required visible captcha (Turnstile + hCaptcha)
$form->addCaptcha('captcha', 'Bot protection')
->setRequired(true);
// 2. Simple required visible captcha with custom required message (Turnstile + hCaptcha)
$form->addCaptcha('captcha', 'Bot protection')
->setRequired('Please verify you are human');
// 3. Simple managed captcha (only for Turnstile)
// The control + label will be rendered but the captcha can be shown or hidden based on Turnstile decision so therefore the JS code shows these messages
$form->addCaptcha('captcha', 'Bot protection')
->setManagedMessages('Check will be performed on background', Html::el('em')->setHtml('Check has been performed successfully.'));
// 4. Invisible captcha (Turnstile + hCaptcha)
// The control + label will be hidden, the only thing that can be shown is required message.
// Turnstile: choose the Invisible widget mode for the site key in the Cloudflare dashboard.
// hCaptcha: rendered as invisible and executed by the bundled JS when the form is submitted.
$form->addCaptcha('captcha', 'Bot protection')
->setInvisible(true);If you need to create the control manually (e.g., for testing):
use DrabekDigital\Captcha\CaptchaControl;
use DrabekDigital\Captcha\CaptchaValidator;
use DrabekDigital\Captcha\Enums\CaptchaType;
// Create validator manually
$validator = new CaptchaValidator('secret-key', CaptchaType::TURNSTILE);
// Create control (validator is the first mandatory parameter)
$captcha = new CaptchaControl($validator, 'Captcha', 'site-key', CaptchaType::TURNSTILE);Server-side verification uses curl when available and falls back to PHP streams (allow_url_fopen). To use your own HTTP client (proxy, retries, loggingβ¦), register any PSR-18 client as a service, it is autowired into the validator. Clients that do not implement the PSR-17 request and stream factories themselves need the factories registered too:
services:
# Symfony HttpClient implements the PSR-17 factories itself
- Symfony\Component\HttpClient\Psr18Client
# Guzzle needs the factories
- GuzzleHttp\Client
- GuzzleHttp\Psr7\HttpFactoryWithout DI pass them to the constructor:
$validator = new CaptchaValidator('secret-key', CaptchaType::TURNSTILE, httpClient: $client, requestFactory: $factory, streamFactory: $factory);When a PSR-3 Psr\Log\LoggerInterface service is registered (e.g. Monolog, or Tracy via Tracy\Bridges\Psr\TracyToPsrLoggerAdapter), it is autowired into the validator:
errorβ the verification service is unavailable or the integration is misconfigured (e.g.invalid-input-secret,sitekey-secret-mismatch)infoβ the visitor's token was rejected (e.g. expired or already used)
Without a logger, errors are written to the PHP error log.
The {captchaScripts} tag adds the nonce automatically. The script helpers accept it as an argument:
{DrabekDigital\Captcha\Assets\CaptchaFrontend::getJavascriptStatic(\DrabekDigital\Captcha\Enums\CaptchaType::TURNSTILE, $nonce)|noescape}
{DrabekDigital\Captcha\Assets\CaptchaFrontend::getLocalJavascriptStatic($nonce)|noescape}Instead of the inline validation script you can serve CaptchaFrontend::getLocalJavascriptPath() as a static file (e.g. copy it to www/ during deploy) and allow it by script-src 'self'.
The providers need these sources in your policy:
- Turnstile:
script-srcandframe-srchttps://challenges.cloudflare.com - hCaptcha:
script-src,frame-src,style-srcandconnect-srchttps://hcaptcha.com https://*.hcaptcha.com
- Only implicit rendering support for both providers (primarily due to reliability).
- For hCaptcha passive mode is not supported at all (no access).
- Custom verification endpoint was not tested.
- Other captcha providers may work but will need custom JS to ensure form validation to work.
MIT License - see LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request, but better to consult via issues before large efforts.
For support, please create an issue on GitHub.