CAPTCHA is actually an acronym for Completely Automated Public Turing Test to Tell Computers and Humans Apart. The technique is similar to the one presented in the preceding recipe, Securing forms with a token. The difference is that instead of storing the token in a hidden form input field, the token is rendered into a graphic that is difficult for an automated attack system to decipher. Also, the intent of a CAPTCHA is slightly different from a form token: it is designed to confirm that the web visitor is a human being, and not an automated system.
Application\Captcha\Phrase class. We also define properties and class constants used in the phrase generation process:namespace Application\Captcha;
class Phrase
{
const DEFAULT_LENGTH = 5;
const DEFAULT_NUMBERS = '0123456789';
const DEFAULT_UPPER = 'ABCDEFGHJKLMNOPQRSTUVWXYZ';
const DEFAULT_LOWER = 'abcdefghijklmnopqrstuvwxyz';
const DEFAULT_SPECIAL =
'¬\`|!"£$%^&*()_-+={}[]:;@\'~#<,>.?/|\\';
const DEFAULT_SUPPRESS = ['O','l'];
protected $phrase;
protected $includeNumbers;
protected $includeUpper;
protected $includeLower;
protected $includeSpecial;
protected $otherChars;
protected $suppressChars;
protected $string;
protected $length;$include* flags are used to signal which character sets will be present in the base string from which the phrase will be generated. For example, if you wish to only have numbers, $includeUpper and $includeLower would both be set to FALSE. $otherChars is provided for extra flexibility. Finally, $suppressChars represents an array of characters that will be removed from the base string. The default removes uppercase O and lowercase l:public function __construct(
$length = NULL,
$includeNumbers = TRUE,
$includeUpper= TRUE,
$includeLower= TRUE,
$includeSpecial = FALSE,
$otherChars = NULL,
array $suppressChars = NULL)
{
$this->length = $length ?? self::DEFAULT_LENGTH;
$this->includeNumbers = $includeNumbers;
$this->includeUpper = $includeUpper;
$this->includeLower = $includeLower;
$this->includeSpecial = $includeSpecial;
$this->otherChars = $otherChars;
$this->suppressChars = $suppressChars
?? self::DEFAULT_SUPPRESS;
$this->phrase = $this->generatePhrase();
}public function getString()
{
return $this->string;
}
public function setString($string)
{
$this->string = $string;
}
// other getters and setters not shown$include* flags and append to the base string as appropriate. At the end, we use str_replace() to remove the characters represented in $suppressChars:public function initString()
{
$string = '';
if ($this->includeNumbers) {
$string .= self::DEFAULT_NUMBERS;
}
if ($this->includeUpper) {
$string .= self::DEFAULT_UPPER;
}
if ($this->includeLower) {
$string .= self::DEFAULT_LOWER;
}
if ($this->includeSpecial) {
$string .= self::DEFAULT_SPECIAL;
}
if ($this->otherChars) {
$string .= $this->otherChars;
}
if ($this->suppressChars) {
$string = str_replace(
$this->suppressChars, '', $string);
}
return $string;
}for() loop, and use the new PHP 7 random_int() function to jump around in the base string:public function generatePhrase()
{
$phrase = '';
$this->string = $this->initString();
$max = strlen($this->string) - 1;
for ($x = 0; $x < $this->length; $x++) {
$phrase .= substr(
$this->string, random_int(0, $max), 1);
}
return $phrase;
}
}Application\Captcha\Phrase. Note that getImage() will return text, text art, or an actual image, depending on which class we decide to use:namespace Application\Captcha;
interface CaptchaInterface
{
public function getLabel();
public function getImage();
public function getPhrase();
}Application\Captcha\Reverse class. The reason for this name is that this class produces not just text, but text in reverse. The __construct() method builds an instance of Phrase. Note that getImage() returns the phrase in reverse:namespace Application\Captcha;
class Reverse implements CaptchaInterface
{
const DEFAULT_LABEL = 'Type this in reverse';
const DEFAULT_LENGTH = 6;
protected $phrase;
public function __construct(
$label = self::DEFAULT_LABEL,
$length = self:: DEFAULT_LENGTH,
$includeNumbers = TRUE,
$includeUpper = TRUE,
$includeLower = TRUE,
$includeSpecial = FALSE,
$otherChars = NULL,
array $suppressChars = NULL)
{
$this->label = $label;
$this->phrase = new Phrase(
$length,
$includeNumbers,
$includeUpper,
$includeLower,
$includeSpecial,
$otherChars,
$suppressChars);
}
public function getLabel()
{
return $this->label;
}
public function getImage()
{
return strrev($this->phrase->getPhrase());
}
public function getPhrase()
{
return $this->phrase->getPhrase();
}
}Application\Captcha\Image class that implements CaptchaInterface. The class constants and properties include not only those needed for phrase generation, but what is needed for image generation as well:namespace Application\Captcha;
use DirectoryIterator;
class Image implements CaptchaInterface
{
const DEFAULT_WIDTH = 200;
const DEFAULT_HEIGHT = 50;
const DEFAULT_LABEL = 'Enter this phrase';
const DEFAULT_BG_COLOR = [255,255,255];
const DEFAULT_URL = '/captcha';
const IMAGE_PREFIX = 'CAPTCHA_';
const IMAGE_SUFFIX = '.jpg';
const IMAGE_EXP_TIME = 300; // seconds
const ERROR_REQUIRES_GD = 'Requires the GD extension + '
. ' the JPEG library';
const ERROR_IMAGE = 'Unable to generate image';
protected $phrase;
protected $imageFn;
protected $label;
protected $imageWidth;
protected $imageHeight;
protected $imageRGB;
protected $imageDir;
protected $imageUrl;$imageDir and $imageUrl. The first is where the graphic will be written. The second is the base URL, after which we will append the generated filename. $imageFont is provided in case we want to provide TrueType fonts, which will produce a more secure CAPTCHA. Otherwise, we're limited to the default fonts which, to quote a line in a famous movie, ain't a pretty sight:public function __construct(
$imageDir,
$imageUrl,
$imageFont = NULL,
$label = NULL,
$length = NULL,
$includeNumbers = TRUE,
$includeUpper= TRUE,
$includeLower= TRUE,
$includeSpecial = FALSE,
$otherChars = NULL,
array $suppressChars = NULL,
$imageWidth = NULL,
$imageHeight = NULL,
array $imageRGB = NULL
)
{imagecreatetruecolor function exists. If this comes back as FALSE, we know the GD extension is not available. Otherwise, we assign parameters to properties, generate the phrase, remove old images, and write out the CAPTCHA graphic:if (!function_exists('imagecreatetruecolor')) {
throw new \Exception(self::ERROR_REQUIRES_GD);
}
$this->imageDir = $imageDir;
$this->imageUrl = $imageUrl;
$this->imageFont = $imageFont;
$this->label = $label ?? self::DEFAULT_LABEL;
$this->imageRGB = $imageRGB ?? self::DEFAULT_BG_COLOR;
$this->imageWidth = $imageWidth ?? self::DEFAULT_WIDTH;
$this->imageHeight= $imageHeight ?? self::DEFAULT_HEIGHT;
if (substr($imageUrl, -1, 1) == '/') {
$imageUrl = substr($imageUrl, 0, -1);
}
$this->imageUrl = $imageUrl;
if (substr($imageDir, -1, 1) == DIRECTORY_SEPARATOR) {
$imageDir = substr($imageDir, 0, -1);
}
$this->phrase = new Phrase(
$length,
$includeNumbers,
$includeUpper,
$includeLower,
$includeSpecial,
$otherChars,
$suppressChars);
$this->removeOldImages();
$this->generateJpg();
}DirectoryIterator class to scan the designated directory and check the access time. We calculate an old image file as one that is the current time minus the value specified by IMAGE_EXP_TIME:public function removeOldImages()
{
$old = time() - self::IMAGE_EXP_TIME;
foreach (new DirectoryIterator($this->imageDir)
as $fileInfo) {
if($fileInfo->isDot()) continue;
if ($fileInfo->getATime() < $old) {
unlink($this->imageDir . DIRECTORY_SEPARATOR
. $fileInfo->getFilename());
}
}
}$imageRGB array into $red, $green, and $blue. We use the core imagecreatetruecolor() function to generate the base graphic with the width and height specified. We use the RGB values to colorize the background:public function generateJpg()
{
try {
list($red,$green,$blue) = $this->imageRGB;
$im = imagecreatetruecolor(
$this->imageWidth, $this->imageHeight);
$black = imagecolorallocate($im, 0, 0, 0);
$imageBgColor = imagecolorallocate(
$im, $red, $green, $blue);
imagefilledrectangle($im, 0, 0, $this->imageWidth,
$this->imageHeight, $imageBgColor);$xMargin = (int) ($this->imageWidth * .1 + .5);
$yMargin = (int) ($this->imageHeight * .3 + .5);
$phrase = $this->getPhrase();
$max = strlen($phrase);
$count = 0;
$x = $xMargin;
$size = 5;
for ($i = 0; $i < $max; $i++) {$imageFont is specified, we are able to write each character with a different size and angle. We also need to adjust the x axis (that is, horizontal) value according to the size:if ($this->imageFont) {
$size = rand(12, 32);
$angle = rand(0, 30);
$y = rand($yMargin + $size, $this->imageHeight);
imagettftext($im, $size, $angle, $x, $y, $black,
$this->imageFont, $phrase[$i]);
$x += (int) ($size + rand(0,5));5, as smaller sizes are unreadable. We provide a low level of distortion by alternating between imagechar(), which writes the image normally, and imagecharup(), which writes it sideways:} else {
$y = rand(0, ($this->imageHeight - $yMargin));
if ($count++ & 1) {
imagechar($im, 5, $x, $y, $phrase[$i], $black);
} else {
imagecharup($im, 5, $x, $y, $phrase[$i], $black);
}
$x += (int) ($size * 1.2);
}
} // end for ($i = 0; $i < $max; $i++)$numDots = rand(10, 999);
for ($i = 0; $i < $numDots; $i++) {
imagesetpixel($im, rand(0, $this->imageWidth),
rand(0, $this->imageHeight), $black);
}md5() with the date and a random number from 0 to 9999 as arguments. Note that we can safely use md5() as we are not trying to hide any secret information; we're merely interested in generating a unique filename quickly. We wipe out the image object as well to conserve memory:$this->imageFn = self::IMAGE_PREFIX
. md5(date('YmdHis') . rand(0,9999))
. self::IMAGE_SUFFIX;
imagejpeg($im, $this->imageDir . DIRECTORY_SEPARATOR
. $this->imageFn);
imagedestroy($im);try/catch block. If an error or exception is thrown, we log the message and take the appropriate action:} catch (\Throwable $e) {
error_log(__METHOD__ . ':' . $e->getMessage());
throw new \Exception(self::ERROR_IMAGE);
}
}getImage() returns an HTML <img> tag, which can then be immediately displayed:public function getLabel()
{
return $this->label;
}
public function getImage()
{
return sprintf('<img src="%s/%s" />',
$this->imageUrl, $this->imageFn);
}
public function getPhrase()
{
return $this->phrase->getPhrase();
}
}Be sure to define the classes discussed in this recipe, summarized in the following table:
|
Class |
Subsection |
The steps it appears in |
|---|---|---|
|
|
Generating a text CAPTCHA |
1 - 5 |
|
|
6 | |
|
|
7 | |
|
|
Generating an image CAPTCHA |
2 - 13 |
Next, define a calling program called chap_12_captcha_text.php that implements a text CAPTCHA. You first need to set up autoloading and use the appropriate classes:
<?php require __DIR__ . '/../Application/Autoload/Loader.php'; Application\Autoload\Loader::init(__DIR__ . '/..'); use Application\Captcha\Reverse;
After that, be sure to start the session. You would use appropriate measures to protect the session as well. To conserve space, we only show one simple measure, session_regenerate_id():
session_start(); session_regenerate_id();
Next, you can define a function that creates the CAPTCHA; retrieves the phrase, label, and image (in this case, reverse text); and stores the value in the session:
function setCaptcha(&$phrase, &$label, &$image)
{
$captcha = new Reverse();
$phrase = $captcha->getPhrase();
$label = $captcha->getLabel();
$image = $captcha->getImage();
$_SESSION['phrase'] = $phrase;
}Now is a good time to initialize variables and determine the loggedIn status:
$image = ''; $label = ''; $phrase = $_SESSION['phrase'] ?? ''; $message = ''; $info = 'You Can Now See Super Secret Information!!!'; $loggedIn = $_SESSION['isLoggedIn'] ?? FALSE; $loggedUser = $_SESSION['user'] ?? 'guest';
You can then check to see whether the login button has been pressed. If so, check to see whether the CAPTCHA phrase has been entered. If not, initialize a message informing the user they need to enter the CAPTCHA phrase:
if (!empty($_POST['login'])) {
if (empty($_POST['captcha'])) {
$message = 'Enter Captcha Phrase and Login Information';If the CAPTCHA phrase is present, check to see whether it matches what is stored in the session. If it doesn't match, proceed as if the form is invalid. Otherwise, process the login as you would have otherwise. For the purposes of this illustration, you can simulate a login by using hard-coded values for the username and password:
} else {
if ($_POST['captcha'] == $phrase) {
$username = 'test';
$password = 'password';
if ($_POST['user'] == $username
&& $_POST['pass'] == $password) {
$loggedIn = TRUE;
$_SESSION['user'] = strip_tags($username);
$_SESSION['isLoggedIn'] = TRUE;
} else {
$message = 'Invalid Login';
}
} else {
$message = 'Invalid Captcha';
}
}You might also want to add code for a logout option, as described in the Safeguarding the PHP session recipe:
} elseif (isset($_POST['logout'])) {
session_unset();
session_destroy();
setcookie('PHPSESSID', 0, time() - 3600);
header('Location: ' . $_SERVER['REQUEST_URI'] );
exit;
}You can then run setCaptcha():
setCaptcha($phrase, $label, $image);
Lastly, don't forget the view logic, which, in this example, presents a basic login form. Inside the form tag, you'll need to add view logic to display the CAPTCHA and label:
<tr> <th><?= $label; ?></th> <td><?= $image; ?><input type="text" name="captcha" /></td> </tr>
Here is the resulting output:

To demonstrate how to use the image CAPTCHA, copy the code from chap_12_captcha_text.php to cha_12_captcha_image.php. We define constants that represent the location of the directory in which we will write the CAPTCHA images. (Be sure to create this directory!) Otherwise, the autoloading and use statement structure is similar. Note that we also define a TrueType font. Differences are noted in bold:
<?php
define('IMAGE_DIR', __DIR__ . '/captcha');
define('IMAGE_URL', '/captcha');
define('IMAGE_FONT', __DIR__ . '/FreeSansBold.ttf');
require __DIR__ . '/../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/..');
use Application\Captcha\Image;
session_start();
session_regenerate_id();Important!
Fonts can potentially be protected under copyright, trademark, patent, or other intellectual property laws. If you use a font for which you are not licensed, you and your customer could be held liable in court! Use an open source font, or one that is available on the web server for which you have a valid license.
Of course, in the setCaptcha() function, we use the Image class instead of Reverse:
function setCaptcha(&$phrase, &$label, &$image)
{
$captcha = new Image(IMAGE_DIR, IMAGE_URL, IMAGE_FONT);
$phrase = $captcha->getPhrase();
$label = $captcha->getLabel();
$image = $captcha->getImage();
$_SESSION['phrase'] = $phrase;
return $captcha;
}Variable initialization is the same as the previous script, and login processing is identical to the previous script:
$image = '';
$label = '';
$phrase = $_SESSION['phrase'] ?? '';
$message = '';
$info = 'You Can Now See Super Secret Information!!!';
$loggedIn = $_SESSION['isLoggedIn'] ?? FALSE;
$loggedUser = $_SESSION['user'] ?? 'guest';
if (!empty($_POST['login'])) {
// etc. -- identical to chap_12_captcha_text.phpEven the view logic remains the same, as we are using getImage(), which, in the case of the image CAPTCHA, returns directly usable HTML. Here is the output using a TrueType font:

If you are not inclined to use the preceding code to generate your own in-house CAPTCHA, there are plenty of libraries available. Most popular frameworks have this ability. Zend Framework, for example, has its Zend\Captcha component class. There is also reCAPTCHA, which is generally invoked as a service in which your application makes a call to an external website that generates the CAPTCHA and token for you. A good place to start looking is http://www.captcha.net/ website.
For more information on the protection of fonts as intellectual property, refer to the article present at https://en.wikipedia.org/wiki/Intellectual_property_protection_of_typefaces.