Skip to content

Captcha PRO

FluentAffiliate Pro can gate the public affiliate registration form behind a captcha challenge. Google reCAPTCHA (v2 checkbox and v3 invisible) ships built in, and the provider layer is pluggable.

Settings live under Settings → Registration → Captcha in the admin SPA.

Architecture

ClassResponsibility
CaptchaManagerSettings storage, provider registry, enable/verify entry points, key-validation bookkeeping
AbstractCaptchaProviderContract every provider implements
RecaptchaProviderBundled Google reCAPTCHA v2/v3 provider (recaptcha key)
CaptchaHandlerWires the provider into the registration form and the registration request

CaptchaHandler::register() hooks three things:

php
add_filter('fluent_affiliate/settings_menu_items', ...);          // reveal the Captcha settings screen
add_action('fluent_affiliate/auth/register_form_after_fields', ...); // render the widget
add_filter('fluent_affiliate/auth/pre_registration_errors', ...);    // verify the response

Settings Shape

Stored in the _captcha_settings option (via Utility):

php
[
    'enabled'         => 'yes',        // 'yes' | 'no'
    'active_provider' => 'recaptcha',  // provider key
    'providers'       => [
        'recaptcha' => [
            'version'            => 'v2_visible',  // 'v2_visible' | 'v3_invisible'
            'v2_site_key'        => '...',
            'v2_secret_key'      => '...',         // encrypted at rest
            'v3_site_key'        => '...',
            'v3_secret_key'      => '...',         // encrypted at rest
            'v3_score_threshold' => 0.5,           // clamped to 0.0–1.0
            'error_message'      => 'Security check failed. Please try again.',
        ],
    ],
]

Secret keys are encrypted with Helper::encryptDecrypt() before storage and are never returned to the browser — getDisplaySettings() swaps them for the __FA_CAPTCHA_SECRET_KEY__ mask. Posting that mask back means "leave the stored secret unchanged".

Key Validation Gate

A captcha with bad keys silently breaks registration, so saving is guarded:

  1. The admin enters keys and presses Validate KeysPOST /settings/captcha/validate.
  2. CaptchaManager::validateCredentials() calls the provider's validateCredentials(). On success it stores a fingerprint (md5 of the sanitized settings) in the _captcha_validated_fingerprint option.
  3. POST /settings/captcha rejects a payload with enabled = yes unless the submitted keys' fingerprint matches that stored fingerprint.

GET /settings/captcha returns validated: true|false so the UI can show whether the active provider's stored keys ever passed.

Verification Flow

CaptchaHandler::verify() runs on fluent_affiliate/auth/pre_registration_errors:

  • It returns early if an earlier filter already rejected the request, or if CaptchaManager::isEnabled() is false (captcha off, or the active provider is not fully configured).
  • Otherwise CaptchaManager::verifyActive($request) delegates to the provider. A failure returns ['message' => CaptchaManager::getErrorMessage()], which the auth handler turns into an HTTP 422.

RecaptchaProvider::verify() fails closed — any unreachable-API or malformed response denies the registration. For v3 it additionally:

  • rejects tokens minted for an action other than affiliate_register, and
  • requires score >= v3_score_threshold (filterable, see below).

REST API

MethodPathPurpose
GET/settings/captchaSettings (secrets masked), provider list, validated flag
POST/settings/captchaSave settings — blocked when enabling unvalidated keys
POST/settings/captcha/validateValidate a site/secret pair against the provider

See Get Captcha Settings, Update Captcha Settings, and Validate Captcha Keys.

Hooks

HookTypePurpose
fluent_affiliate/captcha_providersfilterRegister additional providers
fluent_affiliate/recaptcha_v3_ref_scorefilterOverride the v3 score threshold
fluent_affiliate/recaptcha_remoteipfilterChange or omit the IP sent to Google
fluent_affiliate/auth/pre_registration_errorsfilterThe gate captcha verification plugs into
fluent_affiliate/auth/register_form_after_fieldsactionWhere the widget is printed

Tighten the v3 threshold

php
add_filter('fluent_affiliate/recaptcha_v3_ref_score', function ($threshold) {
    return 0.7;
});

Stop sending visitor IPs to Google

php
add_filter('fluent_affiliate/recaptcha_remoteip', '__return_empty_string');

Adding a Custom Provider

Extend AbstractCaptchaProvider and register the instance. Providers are keyed by getKey(), and anything that is not an AbstractCaptchaProvider instance is discarded.

php
use FluentAffiliatePro\App\Services\Captcha\AbstractCaptchaProvider;

class My_Turnstile_Provider extends AbstractCaptchaProvider
{
    public function getKey()   { return 'turnstile'; }
    public function getTitle() { return 'Cloudflare Turnstile'; }

    public function getDefaults()
    {
        return [
            'site_key'      => '',
            'secret_key'    => '',
            'error_message' => __('Security check failed. Please try again.', 'my-plugin'),
        ];
    }

    public function sanitize($input, $prev = [])
    {
        return [
            'site_key'      => sanitize_text_field($input['site_key'] ?? ''),
            'secret_key'    => sanitize_text_field($input['secret_key'] ?? ($prev['secret_key'] ?? '')),
            'error_message' => sanitize_text_field($input['error_message'] ?? ''),
        ];
    }

    // Hide stored secrets from the admin UI.
    public function maskSecrets($settings)
    {
        if (!empty($settings['secret_key'])) {
            $settings['secret_key'] = '__MASKED__';
        }
        return $settings;
    }

    public function isConfigured($settings)
    {
        return !empty($settings['site_key']) && !empty($settings['secret_key']);
    }

    public function render($settings)
    {
        // echo the widget markup + enqueue the provider script
    }

    public function verify($settings, $request)
    {
        // return true only on a confirmed-valid response — fail closed
    }

    public function validateCredentials($settings, $token)
    {
        // return ['valid' => bool, 'message' => string]
    }
}

add_filter('fluent_affiliate/captcha_providers', function ($providers) {
    $providers['turnstile'] = new My_Turnstile_Provider();
    return $providers;
});

credentialFingerprint() is inherited (an md5 of the settings array) and powers the validate-before-save gate; override it if your provider needs a different notion of "the same keys".

FluentAffiliate developer documentation