Skip to content

Form Spam Protection 2.16 legacy 1.95 ​

Every form submission (contact forms and event registrations) passes two independent checks. The honeypot always runs. reCAPTCHA runs on top of it as soon as keys are configured. Both checks are part of the package, a project only has to render the matching fields in its theme.

LayerActiveCatches
Honeypot fieldalwaysbots that fill in every field
Honeypot timing gatealwaysbots that post faster than a person can type
reCAPTCHAwhen GOOGLE_RECAPTCHA_SECRET is setbots that render the page and behave like a browser

A submission that fails any check is rejected with a validation error. Nothing is stored and no mail goes out.

Honeypot ​

The form view renders two hidden inputs next to the CSRF token:

blade
<input type="text" name="fax_number" value="" style="position: absolute; left: -9999px;" autocomplete="nope">
<input type="hidden" name="user_identification" value="{{ Crypt::encrypt(time()) }}">
  1. fax_number must arrive empty. People never see it, form filling bots usually do.
  2. user_identification carries the encrypted time the form was rendered. The submission must arrive at least evolve.honeypot-threshold seconds later (default 3). A missing or tampered value fails validation.
php
'honeypot-threshold' => env('EVOLVE_HONEYPOT_THRESHOLD', 3),

Projects that still set the older key core.honeypot-threshold keep their value, it takes precedence. Before 2.16 (legacy 1.95) the gate read only that older key, which no config defined, so it caught nothing but submissions within the same second.

With the page cache active the theme refreshes user_identification on every page load through /api/csrf-token, so cached pages do not carry a stale timestamp.

reCAPTCHA ​

Which key type ​

Google issues three kinds of reCAPTCHA keys. The key type is chosen when the key is created in the reCAPTCHA admin console and cannot be changed later.

Key typeWorks with the Evolve themeServer check
v3 (score based)yes, recommendedsuccess and score ≥ threshold
v2 Invisibleyessuccess only, no score
v2 Checkbox ("I'm not a robot")no

The theme binds reCAPTCHA to the submit button (class="g-recaptcha" with data-callback). That binding works for v3 and v2 Invisible keys. A v2 Checkbox key needs a visible widget, which the theme does not render. Visitors see no challenge with either supported type.

Each key is bound to a list of domains. Add the customer's domain (and the staging domain, if it should run there too) to the key in the console. A key used on a domain that is not listed fails every submission.

Configuration ​

Set the keys in the panel under Settings > Google or in the .env:

ini
GOOGLE_RECAPTCHA_SITEKEY=6Lc...
GOOGLE_RECAPTCHA_SECRET=6Lc...
GOOGLE_RECAPTCHA_SCORE=0.5
php
'google' => [
    'recaptcha' => [
        'secret' => env('GOOGLE_RECAPTCHA_SECRET'),
        'site-key' => env('GOOGLE_RECAPTCHA_SITEKEY'),
        'score' => env('GOOGLE_RECAPTCHA_SCORE', 0.5),
    ],
],
  1. Site key switches the theme to the reCAPTCHA submit and makes the head load recaptcha/api.js.
  2. Secret switches the server check on. From then on every submission needs a valid g-recaptcha-response token.
  3. Score is the minimum a v3 token needs, from 0.0 (bot) to 1.0 (human). 0.5 is Google's suggested default. Lower it if real enquiries bounce, raise it if spam still gets through. Values outside 0 to 1 are clamped, anything non numeric falls back to 0.5.

Set site key and secret together. A secret without a site key rejects every submission, because the theme never sends a token.

If Google cannot be reached, the submission is rejected. Before 2.16 (legacy 1.95) the score was not checked at all: any token Google accepted passed, including those scored as bots.

Legacy projects with their own google block

Legacy 1.x merges config/services.php shallowly. A project that defines the whole google key in its published config/services.php loses the package's score key. The check then uses 0.5, but GOOGLE_RECAPTCHA_SCORE and the settings field have no effect until the project adds the score line shown above.

Theme integration ​

The starter theme ships with it. Projects with their own components/form.blade.php need the button switch and a callback that submits the form:

blade
@php
$recaptchaCallback = 'submitForm' . preg_replace('/[^A-Za-z0-9]/', '', $form->name);
@endphp

<form id="form{{ $form->name }}" ...>
    ...
    @if(config('services.google.recaptcha.site-key'))
    <button
        class="g-recaptcha btn btn-primary"
        data-sitekey="{{ config('services.google.recaptcha.site-key') }}"
        data-callback="{{ $recaptchaCallback }}"
        data-action="submit">
        {{ __('evolve::messages.send') }}
    </button>
    @else
    <button type="submit" class="btn btn-primary">{{ __('evolve::messages.send') }}</button>
    @endif
</form>

@if(config('services.google.recaptcha.site-key'))
<script>
    function {{ $recaptchaCallback }}(token) {
        const form = document.getElementById(@js('form' . $form->name));
        let input = form.querySelector('input[name="g-recaptcha-response"]');

        if (! input) {
            input = Object.assign(document.createElement('input'), { type: 'hidden', name: 'g-recaptcha-response' });
            form.appendChild(input);
        }

        input.value = token;
        form.submit();
    }
</script>
@endif

Points to watch:

  1. The id belongs on the <form> only. Older project themes put id="form…" on the wrapping <div> as well. getElementById then returns the div and submit() fails silently.
  2. The callback name must be a valid JavaScript identifier. Form names may contain hyphens, hence the preg_replace.
  3. api.js must be on the page. The Evolve theme loads it in head.blade.php when a site key is set. Projects with their own head (most legacy themes) do not. Without it the button submits nothing and nothing happens. Load it once in the form component:
blade
@once
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
@endonce
  1. Show spam failures. The spam fields have no visible input, so their errors are not rendered next to a field. Without a message the visitor only sees the form again:
blade
@if($errors->hasAny(['g-recaptcha-response', 'user_identification', 'fax_number']))
<div class="text-sm text-red-500">{{ __('Your request could not be verified. Please try again.') }}</div>
@endif

Privacy ​

reCAPTCHA sends visitor data to Google. Mentioning it in the site's privacy policy is the customer's responsibility.

Environment variables ​

VariableDefaultEffect
EVOLVE_HONEYPOT_THRESHOLD3Seconds a form has to be open before a submission is accepted.
GOOGLE_RECAPTCHA_SITEKEYunsetPublic key, switches the theme to the reCAPTCHA submit. Settings panel.
GOOGLE_RECAPTCHA_SECRETunsetSecret key, switches the server check on. Settings panel.
GOOGLE_RECAPTCHA_SCORE0.5Minimum v3 score. Settings panel.

Troubleshooting ​

SymptomLikely cause
Every submission fails after adding keysKey not valid for this domain, v2 Checkbox key, or only the secret set
Real enquiries bounce with the verification messageScore too strict, lower GOOGLE_RECAPTCHA_SCORE
Nothing happens on submitDuplicate id="form…" in the theme, or api.js not loaded (site key missing, or a project head that does not include it)
Spam still arrivesNo secret set (only the honeypot runs), or a v2 Invisible key (no score to check)