Captcha

hostkurd/flocms-captcha adds an image captcha to forms that guests can submit, such as a contact or sign-up form, to keep automated spam out. New FloCMS projects already require it. It needs PHP's GD extension.

How it works

  1. A page shows an image with a random code, for example K7MPQ2. Creating the image stores the code in the visitor's session.
  2. The visitor types the code into the form.
  3. Your controller compares the typed text with the stored code.

Showing the image

Serve the image from a controller action, so it runs with the site's session:

<?php
namespace FloCMS\Controllers;

use FloCMS\Captcha\CaptchaManager;
use FloCMS\Core\Controller;

class CaptchaController extends Controller
{
    public function image(): void
    {
        (new CaptchaManager())->output();
        exit;
    }
}

output() creates a new code, stores it, and sends a PNG with headers that stop browsers from caching it. The exit matters: the action has no view to render.

In the form:

<form method="post" action="">
    @csrf
    <img src="<?= SITE_URI ?>/captcha/image" alt="Security code" width="180" height="50">
    <input type="text" name="captcha" autocomplete="off" required>
    <button type="submit">Send</button>
</form>

A "new code" link only needs to reload the image, for example by adding ? and a timestamp to its src.

Checking the code

use FloCMS\Captcha\CaptchaManager;

public function contact(): void
{
    if (!$this->request->isMethod('POST')) {
        return;
    }

    $captcha = new CaptchaManager();
    $input = $this->request->input('captcha');
    $valid = $captcha->verify(is_string($input) ? $input : null);

    if (!$valid) {
        $this->data['error'] = 'The security code is wrong. Please try again.';
        return;
    }

    // handle the form
}

verify() returns true when the typed text matches. Spaces around it are ignored, and by default upper and lower case don't matter.

Each code can be checked once. verify() removes the stored code on every check, right or wrong, so a script can't keep guessing the same code and a submitted form can't be replayed. After a failed check, the form needs a new image, which it gets when the page shows the form again. Check once per request and keep the result; a second verify() call fails.

Note

Single-use codes arrived in flocms-captcha 1.1.0 (FloCMS 1.7.5). With 1.0.0, verify() kept the code, so you had to call clear() after every check. Calling clear() after verify() still works; it's just no longer needed.

A captcha doesn't replace CSRF protection; keep @csrf in the form. See sessions and CSRF.

Configuration

Pass the settings you want to change. Use the same settings for output() and verify(), or at least the same session_key:

$captcha = new CaptchaManager([
    'length' => 5,
    'width' => 200,
    'height' => 60,
]);
Setting Default
length 6 Number of characters
width, height 180, 50 Image size in pixels
font_size 24
angle_min, angle_max -15, 15 Rotation range of each character
noise_lines 8 Lines drawn across the image
characters ABCDEFGHJKLMNPQRSTUVWXYZ23456789 The characters to pick from; easily confused ones like 0, O, 1 and I are left out
case_sensitive false
session_key flocms_captcha Where the code is stored in the session
single_use true verify() removes the code on every check. Set false only if you must check the same code twice, and call clear() yourself.
fonts three fonts in the package TrueType font files to pick from

The code is generated with random_int() and compared with hash_equals().

Challenge lifecycle

output() generates and stores a new challenge and renders its image. refresh() generates/stores a new code and returns the text; it does not render an image. Don't return that text to the visitor as a refresh endpoint. Reload the image endpoint instead.

One session_key stores one code, so refreshing a second form or browser tab using the same key replaces the first form's challenge. Use distinct configured keys for independent forms and the same key for each form's image and verification endpoints.

The package has no challenge timestamp or automatic expiry. clear() removes the code, and so does verify() with the default single_use; session expiry removes session data according to PHP's settings. If a form needs a shorter lifetime, record and check a timestamp in your application as well.

getCaptcha() exposes the underlying Captcha object: generateText() generates without storing, store($text) stores, getStoredText() reads, and getConfig() returns merged configuration. These are server-side utilities; do not expose stored answers to the client.

Your own renderer

To draw the captcha differently, implement FloCMS\Captcha\Renderer\RendererInterface and pass it as the second argument:

use FloCMS\Captcha\Renderer\RendererInterface;

class CustomRenderer implements RendererInterface
{
    public function render(string $text): void
    {
        // send headers and output an image of $text
    }
}

$captcha = new CaptchaManager([], new CustomRenderer());

Captchas in the API

The captcha is stored in the session, and the API has no session by default. Using this CAPTCHA through an API requires deliberate session/cookie integration; it does not become token-based automatically. API rate limits help limit abuse, while idempotency handles safe retries and does not verify that a client is human. See CORS and rate limiting.

Esc