CaptchaFun Lite

Fun & Secure PHP Captcha — Single-file, no database, no dependencies.

PHP 8.1+ Single File No Database 5 CAPTCHA Types 5 Themes GDPR Ready

Table of Contents

  1. Installation
  2. Quick Start
  3. Basic Usage
  4. AJAX Form Integration
  5. CAPTCHA Types
  6. Configuration
  7. Themes & Dark Mode
  8. Security Features
  9. Public API
  10. JavaScript Events
  11. Multiple CAPTCHAs
  12. Languages

Installation

CaptchaFun Lite is a single PHP file. No Composer, no framework, no database required.

Step 1: Copy the file

Upload captchafun.php to your project directory.

Step 2: Change the secret key

Open the file and change the secret_key value in the configuration array at the top:

'secret_key' => 'your-unique-random-string-here',
⚠️ Important Always change the default secret key before deploying. Use a long random string (32+ characters).

Step 3: Done!

That's it. Include the file in your project and start using it.

Quick Start

The minimal code to protect a form:

<?php require_once 'captchafun.php'; ?>

<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!CaptchaFun::verify($_POST, 'my_form')) {
        $error = CaptchaFun::getLastError();
    } else {
        $success = true;
    }
}
?>

<form method="post">
    <input type="text" name="name" placeholder="Name" required>
    <input type="email" name="email" placeholder="Email" required>

    <?= CaptchaFun::render('my_form') ?>

    <button type="submit">Submit</button>
</form>

Basic Usage

Rendering

Call CaptchaFun::render() inside your form, passing a unique form identifier:

<?= CaptchaFun::render('contact_form') ?>

The form ID must be unique per form on the page. It links the rendered challenge to the verification step.

Verification

In your form processing logic, call CaptchaFun::verify():

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!CaptchaFun::verify($_POST, 'contact_form')) {
        // CAPTCHA failed
        echo CaptchaFun::getLastError();
    } else {
        // CAPTCHA passed — process the form
        // send email, save data, etc.
    }
}

Error Messages

After a failed verification, get the human-readable error message:

$error = CaptchaFun::getLastError();
// Returns translated string like "Incorrect answer. Try again."

AJAX Form Integration

✅ Yes, CaptchaFun works with AJAX! You can submit forms via AJAX and validate the CAPTCHA server-side. The response is returned as JSON.

How it works

  1. Render the CAPTCHA normally in your HTML form
  2. Submit the entire form (including hidden CAPTCHA fields) via AJAX
  3. Server validates with CaptchaFun::verify() and returns JSON
  4. On failure, refresh the CAPTCHA widget using CaptchaFunJS.refresh()

Complete AJAX Example

Backend: ajax-handler.php

<?php
require_once 'captchafun.php';

header('Content-Type: application/json');

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    echo json_encode(['success' => false, 'error' => 'Invalid method']);
    exit;
}

// Verify CAPTCHA first
if (!CaptchaFun::verify($_POST, 'ajax_form')) {
    echo json_encode([
        'success' => false,
        'error' => CaptchaFun::getLastError(),
        'refresh_captcha' => true
    ]);
    exit;
}

// CAPTCHA passed — process form data
$name = htmlspecialchars(trim($_POST['name'] ?? ''));
$email = filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL);

if (!$name || !$email) {
    echo json_encode(['success' => false, 'error' => 'Invalid form data.']);
    exit;
}

// Do something: send email, save to DB, etc.

echo json_encode([
    'success' => true,
    'message' => 'Form submitted successfully!'
]);
exit;

Frontend: HTML + JavaScript

<?php require_once 'captchafun.php'; ?>
<!DOCTYPE html>
<html>
<head>
    <title>AJAX Form with CaptchaFun</title>
</head>
<body>

<form id="ajaxForm">
    <input type="text" name="name" placeholder="Name" required>
    <input type="email" name="email" placeholder="Email" required>
    <textarea name="message" placeholder="Message"></textarea>

    <?= CaptchaFun::render('ajax_form') ?>

    <button type="submit">Send</button>
    <div id="formMessage"></div>
</form>

<script>
document.getElementById('ajaxForm').addEventListener('submit', function(e) {
    e.preventDefault();

    var form = this;
    var formData = new FormData(form);
    var messageEl = document.getElementById('formMessage');

    // Disable button during submission
    var btn = form.querySelector('button[type="submit"]');
    btn.disabled = true;
    btn.textContent = 'Sending...';

    fetch('ajax-handler.php', {
        method: 'POST',
        body: formData
    })
    .then(function(response) { return response.json(); })
    .then(function(data) {
        if (data.success) {
            messageEl.innerHTML = '<p style="color:green">' + data.message + '</p>';
            form.reset();
            // Refresh CAPTCHA for next submission
            CaptchaFunJS.refresh('ajax_form');
        } else {
            messageEl.innerHTML = '<p style="color:red">' + data.error + '</p>';
            // Refresh CAPTCHA on failure (challenge is single-use)
            if (data.refresh_captcha) {
                CaptchaFunJS.refresh('ajax_form');
            }
        }
    })
    .catch(function(err) {
        messageEl.innerHTML = '<p style="color:red">Network error. Try again.</p>';
    })
    .finally(function() {
        btn.disabled = false;
        btn.textContent = 'Send';
    });
});
</script>

</body>
</html>

Key Points for AJAX

CAPTCHA Refresh Endpoint

CaptchaFun has a built-in AJAX refresh handler. When CaptchaFunJS.refresh() is called, it requests:

GET captchafun.php?action=refresh&form_id=ajax_form

This returns a JSON response with new challenge HTML that replaces the current widget contents.

💡 Tip The refresh URL uses the same captchafun.php file. Make sure it's accessible via HTTP (not just included via PHP).

Using Fetch API with JSON body

If you prefer sending JSON instead of FormData, convert the form data first:

var formData = new FormData(form);
var jsonData = {};
formData.forEach(function(value, key) { jsonData[key] = value; });

fetch('ajax-handler.php', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams(jsonData).toString()
});
⚠️ Important The server reads from $_POST, so send data as application/x-www-form-urlencoded or multipart/form-data (FormData does this automatically). If you send application/json, you'll need to decode it manually with json_decode(file_get_contents('php://input'), true).

CAPTCHA Types

🔢

Math

Simple arithmetic questions. Difficulty configurable: addition, subtraction, or multiplication.

😀

Emoji

Click the correct emoji from a selection. Fun and visual challenge.

🎨

Color

Select the named color from colored circles. Quick and intuitive.

↔️

Slider

Drag a slider to the end. Validated server-side with timing checks.

🍯

Honeypot

Invisible trap field. Catches bots that auto-fill all fields.

🎲

Random

Randomly selects from available types on each page load.

Forcing a specific type

// In the config array:
'captcha_type' => 'emoji',  // Always show emoji CAPTCHA

Random mode (default)

// Rotates between configured types:
'captcha_type' => 'random',
'available_types' => ['math', 'emoji', 'slider', 'color'],

Configuration

All settings are in the $CAPTCHAFUN_CONFIG array at the top of captchafun.php:

Option Type Default Description
enabled bool true Enable/disable CAPTCHA globally
captcha_type string 'random' Type to show: math, emoji, color, slider, random
available_types array ['math','emoji','slider','color'] Types available for random mode
difficulty string 'easy' Math difficulty: easy, medium, hard
theme string 'colorful' Visual theme: minimal, colorful, dark, glass, fun
dark_mode bool false Enable dark color scheme
min_submit_time int 3 Minimum seconds before valid submission
challenge_expiration int 300 Seconds until challenge expires
max_attempts int 5 Failed attempts before lockout
lockout_time int 900 Lockout duration in seconds (15 min)
honeypot_enabled bool true Enable invisible honeypot field
rate_limit_enabled bool true Enable rate limiting
language string 'pt' Language: pt, en, es, fr
secret_key string Secret for HMAC signing (CHANGE THIS!)

Themes & Dark Mode

Minimal

Clean, subtle borders, no shadows. Fits anywhere.

🌈

Colorful

Gradient background, vibrant indigo accent. Default theme.

🌙

Dark

Deep dark background, purple accent. Native dark theme.

🪟

Glass

Glassmorphism effect with backdrop blur. Modern and elegant.

🎉

Fun

Dashed borders, warm colors, playful feel.

Setting a theme

'theme' => 'glass',  // Options: minimal, colorful, dark, glass, fun

Dark mode

Enable dark mode alongside any theme:

'dark_mode' => true,

Dark mode adapts colors while preserving the chosen theme's personality.

Security Features

🔐 HMAC-Signed Tokens

Every challenge generates a signed token using hash_hmac('sha256', ...). The token binds the challenge ID, form ID, and timestamp. Tampered tokens are rejected.

🔄 Single-Use Challenges

Each challenge can only be verified once. After verification (success or failure), the challenge is removed from the session. Replay attacks are impossible.

⏱️ Minimum Submission Time

Forms submitted faster than min_submit_time seconds are rejected. Bots that instantly submit forms are caught.

⏰ Challenge Expiration

Challenges expire after challenge_expiration seconds (default: 5 minutes). Old challenges are automatically cleaned from the session.

🚫 Rate Limiting & Lockout

After max_attempts failed tries, the user is locked out for lockout_time seconds. Prevents brute force attacks.

🍯 Honeypot Trap

An invisible field catches bots that auto-fill all inputs. The field is positioned off-screen with aria-hidden and tabindex="-1".

🖥️ Server-Side Only Validation

Correct answers are never exposed in HTML or JavaScript. All validation happens on the server. Disabling JS doesn't bypass the CAPTCHA.

Public API Reference

CaptchaFun::render(string $formId): string

Renders the CAPTCHA widget HTML for a given form.

ParameterTypeDescription
$formIdstringUnique identifier for this form (default: 'default')

Returns: HTML string to echo inside your form.

CaptchaFun::verify(array $postData, string $formId): bool

Validates the submitted CAPTCHA response.

ParameterTypeDescription
$postDataarrayThe $_POST superglobal (or equivalent)
$formIdstringMust match the ID used in render()

Returns: true if valid, false if invalid.

CaptchaFun::getLastError(): string

Returns the last error message after a failed verify() call. The message is already translated to the configured language.

CaptchaFun::resetChallenge(string $formId): void

Removes all stored challenges for a specific form. Useful when programmatically resetting a form.

JavaScript: CaptchaFunJS.refresh(formId)

Refreshes the CAPTCHA widget via AJAX. Fetches a new challenge from the server and replaces the current one.

// Refresh after failed AJAX submission
CaptchaFunJS.refresh('ajax_form');

JavaScript Events

CaptchaFun dispatches custom events on the document element:

EventWhenDetail
captchafun:loaded Widget initialized on page {}
captchafun:reset After AJAX refresh completes { formId: '...' }

Listening to events

document.addEventListener('captchafun:loaded', function() {
    console.log('CaptchaFun is ready!');
});

document.addEventListener('captchafun:reset', function(e) {
    console.log('CAPTCHA refreshed for form:', e.detail.formId);
});

Multiple CAPTCHAs on One Page

Use different form IDs for each CAPTCHA instance:

<!-- Login form -->
<form method="post" action="login.php">
    <input name="username">
    <input name="password" type="password">
    <?= CaptchaFun::render('login_form') ?>
    <button>Login</button>
</form>

<!-- Registration form -->
<form method="post" action="register.php">
    <input name="email">
    <input name="password" type="password">
    <?= CaptchaFun::render('register_form') ?>
    <button>Register</button>
</form>

Verify each one independently:

CaptchaFun::verify($_POST, 'login_form');
CaptchaFun::verify($_POST, 'register_form');

Languages

CaptchaFun includes 4 languages out of the box:

CodeLanguage
ptPortuguese
enEnglish
esSpanish
frFrench

Setting the language

'language' => 'en',

Adding a new language

Add a new key to the $translations array inside the CaptchaFun class, following the existing pattern (copy the 'en' block and translate).

Requirements