Ajax captcha Integration

CAPTCHA (Completely Automated Public Turing test to tell Computers and Humans Apart) is a popular method used to prevent automated bots from performing actions on websites. In this article, we'll explore how to implement an Ajax-based CAPTCHA in a CodeIgniter 4 PHP application. We'll use JavaScript for handling Ajax requests and responses. In this example when you click on subscribe catpcha submit button ajax request will be send to captcha controller function , it will verify and check the captcha if wrong captcha found it will automatic refresh captcha without page load and form will not submit. we will store captcha string in mysql database and validate accordingly.

Step 1: Setup CodeIgniter 4 Project

Prerequisites

Before starting, ensure you have the following:

  • PHP 7.x or later installed
  • CodeIgniter 4 framework set up
  • JavaScript (preferably jQuery) included in your project
Setting Up the Project

  1. Create a New CodeIgniter 4 Project  If you haven't set up a CodeIgniter 4 project yet, you can follow the official installation guide.
  2. Include jQuery Library  Add the jQuery library to your project. You can include it via CDN in your main layout file.

we have already running wampp on my window pc and codeigniter 4 has been installed and also virtual hosts has been configured. so we will take an example of captcha subscription form where a email field and captcha verification field needed for users to subscribe. You can set your base url as per your application or as per your virutal host you configured. Here in this example the entry point will be this url http://ivt.localhost.in/subscribe, which will display the captcha form or captcha subscribe form.

Ajax captcha using CodeIgniter PHP

									                 
https://code.jquery.com/jquery-3.6.0.min.js
https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css										
									

Step 2: Create Database Table

We need a table to store CAPTCHA tokens. Create a table named captchas with the following mysql schema:

									     
SET NAMES utf8;
SET time_zone = '+00:00';
SET foreign_key_checks = 0;
SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO';

DROP TABLE IF EXISTS `captchas`;
CREATE TABLE `captchas` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `captcha_string` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci DEFAULT NULL,
  `verified` tinyint(1) DEFAULT '0',
  `created_at` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;										
									

Step 3: Create Model

Create a new model named CaptchaModel.php inside the app/Models directory.Open Terminal or Command Prompt: Navigate to your CodeIgniter 4 project directory using the terminal or command prompt. php spark make:model CaptchaModel After running the command, CodeIgniter will create a new model file under the app/Models directory with the name CaptchaModel.php

Your new model will have a basic structure like this:

									     
<?php
namespace App\Models;
use CodeIgniter\Model;
class CaptchaModel extends Model
{
    protected $table = 'captchas';
    protected $primaryKey = 'id';
    protected $allowedFields = ['captcha_string', 'verified','created_at'];

	public function validateToken($token)
    {
		$query = $this->where('captcha_string', $token)
                      ->where('created_at >', date('Y-m-d H:i:s', strtotime('-5 minutes')))
                      ->where('verified',0)
					  ->first();
					  
        return $query !== null;
    }
	
	public function setVerified($token){
		$existingRecord = $this->where('captcha_string', $token)->where('verified',0)->where('created_at >', date('Y-m-d H:i:s', strtotime('-5 minutes')))->first();
		if($existingRecord){
		   $this->update($existingRecord['id'], ['verified' => true]);
		}else{
		  log_message('error', 'Captcha token not found in the database: ' . $token);
		}
	    return $existingRecord !== null;
	}
}										
									

Step 4: Create Controller

Create a new controller named CaptchaController.php inside the app/Controllers directory. Just like you created model and verify that same will continue like you go to terminal and in command line point to your project folder and run the command php spark make:controller CaptchaController After running the command, CodeIgniter will create a new controller file under the app/Controllers directory with the name CaptchaController.php. You can also include form validation in functions.

Your new controller will have a basic structure like below:

  1. The validateCaptcha() function retrieves the captcha token from the request.
  2. It validates the token using your existing validation logic (assuming validateToken() returns true if the token is valid).
  3. If the token is valid, it updates the database by calling method setVerified() on the CaptchaModel passing the captcha token.
  4. Finally, it returns a JSON response indicating whether the captcha validation was successful.
  5. We need to ensure that generateImage(), it generates the captcha image and returns the image data in a way that can be embedded in the JSON response.
  6. The image data is captured using output buffering (ob_start() and ob_get_clean()) and set as the response body. This way, the image is returned as the response instead of being directly outputted to the browser.

									     
<?php
namespace App\Controllers;
use App\Models\CaptchaModel;
use CodeIgniter\Controller;
class CaptchaController extends Controller
{
    public function index()
    {
        helper("form");
        return view("captcha_form");
    }
    public function validateCaptcha()
    {
        $request = service("request");
        $captcha_input = $request->getPost("captcha_input");
        $t_input = $request->getPost("t_input");
        $token_url = basename($t_input);
        $token = base64_decode($token_url);
        $model = new CaptchaModel();
        $isValid = $model->validateToken($captcha_input);

        if ($isValid && $token === $captcha_input) {
            $model->setVerified($token);
            return json_encode(["success" => true]);
        } else {
            return json_encode(["success" => false]);
        }
    }

    public function generateCaptchaImage($captchaToken = null)
    {
        $captchaString = base64_decode($captchaToken);
        $captchaModel = new CaptchaModel();
        $captchaModel->insert([
            "captcha_string" => $captchaString,
            "created_at" => date("Y-m-d H:i:s"),
            "verified" => false,
        ]);

        $image = imagecreatetruecolor(200, 50);
        $bgColor = imagecolorallocate($image, 255, 255, 255);
        $textColor = imagecolorallocate($image, 0, 0, 0);
        imagefill($image, 0, 0, $bgColor);
        imagettftext(
            $image,
            20,
            0,
            50,
            30,
            $textColor,
            FCPATH . "/webfonts/fa-solid-900.ttf",
            $captchaString
        );

        $response = service("response");
        $response->setContentType("image/png");

        ob_start();
        imagepng($image);
        $imageData = ob_get_clean();

        return $response->setBody($imageData);
    }

    public function generateImage()
    {
        $response = service("response");

        $randomBytes = random_bytes(3);
        $hexString = bin2hex($randomBytes);
        $numericString = preg_replace("/[^1-9]/", "", $hexString);
        $numericString = ltrim($numericString, "0");
        $numericString = substr($numericString, 0, 5);
        $numericString = str_pad(
            $numericString,
            5,
            mt_rand(1, 9),
            STR_PAD_RIGHT
        );
        $captchaString = $numericString;

        $image = imagecreatetruecolor(200, 50);
        $bgColor = imagecolorallocate($image, 255, 255, 255);
        $textColor = imagecolorallocate($image, 0, 0, 0);
        imagefill($image, 0, 0, $bgColor);
        imagettftext(
            $image,
            20,
            0,
            50,
            30,
            $textColor,
            FCPATH . "/webfonts/fa-solid-900.ttf",
            $captchaString
        );

        ob_start();
        imagepng($image);
        $imageData = ob_get_clean();

        $captchaToken = base64_encode($captchaString);
        $captchaImageUrl =
            base_url("captcha/generateCaptchaImage") . "/" . $captchaToken;
        $responseBody = [
            "image_data" => base64_encode($imageData),
            "image_url" => $captchaImageUrl,
        ];
        return $response->setBody(json_encode($responseBody));
    }
}										
									

Step 5: Create the View File

Create a view file named captcha_form.php in the app/Views directory.

When page loads done generateCaptcha function call ajax which will return json and have captcha image url , In our captcha form we have image input , so return image url will be set by jQuery in successfull return. The return url always point to controller function generateCaptchaImage, this function generate random captcha images.

									                                        
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>CAPTCHA Form Example</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="shortcut icon" type="image/png" href="/favicon.ico">
	<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" >
</head>
<body>
<div class="container py-3">
 <main>
    <div class="col">
	<div class="card mb-4 rounded-3 shadow-sm border-primary" style="width: 40rem;margin:0 auto;">
	<?php $attributes = ['class' => '', 'method' => "post", 'id' => 'saveprofile',];
									echo form_open(base_url() . 'captcha/validate/', $attributes); ?>
	  <div class="card-header py-3 text-white bg-primary border-primary">
		<h4 class="my-0 fw-normal">Captcha Form Example</h4>
	  </div>
	  <div class="card-body">
		  <div class="row mb-3">
			<label for="email" class="col-sm-2 col-form-label">Email</label>
			<div class="col-sm-10">
			  <input type="email" class="form-control" id="email">
			</div>
		  </div>
		   
		  <div class="row mb-3">
			<label for="captcha_img" class="col-sm-2 col-form-label"></label>
			<div class="col-sm-10">
			 <img id="captcha_img" alt="CAPTCHA Image" class="border border-info img-fluid" width="30%" height="35px" style="width:30%;height:35px;">
			</div>
		  </div>
		  <div class="row mb-3">
			<label for="verify_humans" class="col-sm-2 col-form-label">Verify?</label>
			<div class="col-sm-10">
			  <input type="text" class="form-control" id="captcha_input" name="captcha_input" required>
			</div>
		  </div>
		  <input type="hidden" id="t_input" name="t_input" required>
	  </div>
	  <div class="card-footer text-center">
		<button class="btn btn-primary" id="submit_btn" type="submit" >Subscribe</button>
	 </div>
	 <?= form_close() ?>
	</div>
	</div>
	</div>
 </main>	
</div>
<script>
	$(document).ready(function() {
        generateCaptcha();
    });
	function generateCaptcha() {
        $.ajax({
            type: "GET",
            url: "<?= base_url('captcha/generateImage') ?>",
            success: function(response) {
                var result = JSON.parse(response);
				$('#captcha_img').attr('src', result.image_url);
				$('#t_input').val(result.image_url);
                $('#captcha_input').val('');
            },
            error: function(xhr, status, error) {
                console.error("Error generating captcha:", error);
            }
        });
    }

    document.addEventListener("DOMContentLoaded", function() {
            var form = document.getElementById("saveprofile");
			form.addEventListener("submit", function(event) {
                event.preventDefault();
				$.ajax({
					type: "POST",
					url: "<?= base_url('captcha/validate') ?>",
					data: $('#saveprofile').serialize(),
					success: function(response) {
						var result = JSON.parse(response);
						if (result.success) {
							alert("your suscription has been done.");
							$('#captcha_input').val('');
							generateCaptcha();
						} else {
alert("Invalid captcha.");							generateCaptcha();
						}
					}
	   });
        });
    });
 </script>
</body>
</html>										
									

Step 6: Add Route

We will add below route to app\Config\Routes.php.

									     
$routes->get('/subscribe', 'CaptchaController::index');
$routes->post('/captcha/verify', 'CaptchaController::verify');
$routes->get('/captcha/generateImage', 'CaptchaController::generateImage');
$routes->post('/captcha/generateImage', 'CaptchaController::generateImage');
$routes->post('/captcha/validate', 'CaptchaController::validateCaptcha');
$routes->post('/captcha/generateCaptchaImage', 'CaptchaController::generateCaptchaImage');
$routes->get('/captcha/generateCaptchaImage/(:any)', 'CaptchaController::generateCaptchaImage/$1');										
									

Conclusion

In this article, we learned how to implement an AJAX CAPTCHA in a CodeIgniter 4 application. We created a database table to store CAPTCHA tokens, a model to interact with the database, a controller to handle CAPTCHA generation and verification, and a view to display the CAPTCHA form and handle AJAX requests. This implementation provides a simple and effective way to protect your web application from bots while providing a smooth user experience.