Componette

Componette

DrabekDigital

DrabekDigital / captcha

πŸ€– Nette Forms extension for Cloudflare Turnstile and hCaptcha integration

download-cloud-line composer require drabek-digital/captcha

Nette Captcha Extension

A comprehensive Nette Forms extension for integrating Cloudflare Turnstile and hCaptcha with your Nette applications.

Features

  • βœ… 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

Requirements

  • PHP: 8.3+
  • Nette Forms: 3.2+
  • Extensions: curl (used by default) or allow_url_fopen for server-side verification

Installation

Install via Composer:

composer require drabek-digital/captcha

Configuration

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

Configuration Options

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)

Usage

Basic Usage

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...
};

Advanced Usage

// 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);

Manual Instantiation (Advanced)

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

Custom HTTP Client

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\HttpFactory

Without DI pass them to the constructor:

$validator = new CaptchaValidator('secret-key', CaptchaType::TURNSTILE, httpClient: $client, requestFactory: $factory, streamFactory: $factory);

Logging

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.

Content Security Policy

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-src and frame-src https://challenges.cloudflare.com
  • hCaptcha: script-src, frame-src, style-src and connect-src https://hcaptcha.com https://*.hcaptcha.com

Limitations

  • 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.

License

MIT License - see LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request, but better to consult via issues before large efforts.

Support

For support, please create an issue on GitHub.

No release at this moment. Try to create first one.

price-tag-2-line

Badges

guide-fill

Dependencies

php (^8.2)
nette/di (^3.0)
nette/utils (^3.0 || ^4.0)
Componette Componette felix@nette.org