Human user verification image class
Human user verification image class
To prevent bots form submitting online-forms, it is a commonly used technique to verify "human user" with a code that has to be entered by reading letters and numbers in a noisy image. This class is a simple way to do this:
Um Bots davon abzuhalten, Online-Formulare abzuschicken oder sich automatisch zu registrieren, werden mit den Formularen üblicherweise verrauschte Bilder mitgeschickt, welche einen zufälligen Code aus Zahlen und Buchstaben enthalten. Der Nutzer muss diesen Code entziffern und eingeben. Mit dieser Klasse kann diese Technik auf einfache Weise angewandt werden:
Sample source code
Anwendungsbeispiel
<?php
// Before doing anything else, we include the library
require_once('HumanUserVerification.class.php');
// This is the entry key we use for the $_SESSION for this sample code
$sessionKey = 'huv-image';
if(isset($_GET['img'])) {
//
// This section sends the imgage (we use the same script for sending HTML
// and the binary image.
// Check referrer and valid session
if(isset($_SERVER['HTTP_REFERER']) && strpos(strtolower($_SERVER['HTTP_REFERER']),
strtolower($_SERVER['HTTP_HOST'])) !== false
&& isset($_SESSION[$sessionKey])
&& is_array($_SESSION[$sessionKey])
&& isset($_SESSION[$sessionKey]['text'])
&& strlen(trim($_SESSION[$sessionKey]['text'])) > 0
){
// Retrieve the data saved in the session and set the corresponding
// instance properties
$cfg = &$_SESSION[$sessionKey];
$vi = new HumanUserVerificationImage(trim($cfg['text']));
if(isset($cfg['width'])) try { $vi->setWidth($cfg['width']); } catch(Exception $e) { ; }
if(isset($cfg['height'])) try { $vi->setHeight($cfg['height']); } catch(Exception $e) { ; }
if(isset($cfg['noise'])) try { $vi->setNoise($cfg['noise']); } catch(Exception $e) { ; }
// Finally send the image as binay code to the browser and exit.
$vi->sendImage();
}
/*
*
* Alternatively, you can use the static function to create an image
* and save the corresponding SHA1 hash code in a cookie:
*
* HumanUserVerificationImage::sendValidationImageAndExit();
*
* This method is much easier to use, but less configurable.
*
*/
// exit if the $vi->sendImage() method did not do this yet.
exit();
}
////////////////////////////////////////////////////////////////////////////////
// This section is the normal HTML script:
//
// Create the instance
$vi = new HumanUserVerificationImage();
// Check if the user has entered the code
if(isset($_GET['input'])) {
try {
// Verify the code saved in the session with the entered code.
$vi->setText($_SESSION[$sessionKey]['text']);
if($vi->verifyInput($_GET['input'])) {
$inputCorrect = "Input is correct";
} else {
$inputCorrect = "Input is not correct";
}
} catch(Exception $e) {
$inputCorrect = "Exception:" . $e->getMessage();
}
/*
* Alternatively:
*
* if(HumanUserVerificationImage::verifyValidationImage($_GET['input'])) {
* $inputCorrect = "Input is correct";
* } else {
* $inputCorrect = "Input is not correct";
* }
*
* Use this static function to verify images sent using the function
* HumanUserVerificationImage::sendValidationImageAndExit();
*/
}
// Set a new random code with 8 characters from the character list
$vi->setRandomText(8, 'ABCDEFGHIJ0123456789klmnopqr?-');
// Write the session for the image lookup
$_SESSION[$sessionKey] = array('text' => $vi->getText());
// And print it all ...
$text = $vi->getText();
print <<<HERE
<html><body>
<style>
body { font-family: monospace; }
table { border-collapse:collapse; border: solid 1px black; padding: 2px; }
td { border: solid 1px black; padding: 5px; }
</style>
<table>
<tr><td>Original text</td><td>$text</td></tr>
<tr><td>Image</td><td><img src="{$_SERVER['PHP_SELF']}?img" /></td></tr>
<tr><td>Enter here</td><td>
<form method="GET" action="{$_SERVER['PHP_SELF']}">
<input type="text" name="input" value="" />
<input type="submit" name="validate" value="validate">
</form>
</td></tr>
<tr><td>Last value was ok:</td><td>$inputCorrect</td></tr>
</table>
</body></html>
HERE;
?>
Output
The output will be a simple table, which contains the plain text code, the image (showing the same code), a form where you can enter the code and a row, which shows if the entered code was correct.
Ausgabe
Die Ausgabe wird eine simple Tabelle sein, welche den zufällig generierten Code als Text und Bild enthält. Außerdem ist ein kleines Formular dabei, in dem der Code eingegeben werden kann. Im Feld darunter wird angezeigt, ob die Eingabe richtig war.
Class source code
Klassen-Quelltext
<?php
/**
* Creates a verification image for human user verification.
* @gpackage de.atwillys.sw.php.misc
* @author Stefan Wilhelm
* @copyright Stefan Wilhelm, 2007
* @license GPL
* @version 1.0
*/
namespace sw;
class HumanUserVerificationImage {
/**
* Class configuration
* @var array
*/
private static $config = array(
);
/**
* The width of the image, 0=auto
* @var int
*/
private $width = 0;
/**
* The height of the image
* @var int
*/
private $height = 15;
/**
* The text of the image.
* @var string
*/
private $text = '';
/**
* Noise factor
* @var double
*/
private $noise = 0.1;
public static function log($what) {
// add logging code here
}
/**
* Sends a verification image to the browser (including a SHA1 hash cookie).
* The function ::verifyValidationImage() can be used to validate the user
* input generated in this function.
* @return void
* @param bool $saveInSession=false
* @param int $textLength
*/
public static function sendValidationImageAndExit($saveInSession=true, $textLength=8) {
OutputBuffer::purge();
if (isset($_SERVER['HTTP_REFERER']) && strpos(strtolower($_SERVER['HTTP_REFERER']), strtolower($_SERVER['HTTP_HOST'])) !== false) {
try {
$vi = new self();
if (isset($_SESSION[$k]['width']))
$vi->setWidth($_SESSION[$k]['width']);
if (isset($_SESSION[$k]['height']))
$vi->setHeight($_SESSION[$k]['height']);
if (isset($_SESSION[$k]['noise']))
$vi->setNoise($_SESSION[$k]['noise']);
$vi->setRandomText($textLength);
setcookie("hvimage", sha1(strtoupper($vi->getText())), time() + 30);
$key = strtolower(__CLASS__);
if ($saveInSession) {
$_SESSION[$key] = $vi->getText();
} else if (isset($_SESSION[$key])) {
unset($_SESSION[$key]);
}
$vi->sendImage();
} catch (\Exception $e) {
// Send error image?
}
}
exit();
}
/**
* Hard-validates the text of a validation image that the user has entered.
* There is no tolerance according to O<-->0 etc. The function uses cookies.
* @param string $input
* @return book
*/
public static function verifyValidationImage($input) {
$key = strtolower(__CLASS__);
if (isset($_SESSION[$key])) {
$vi = new self();
$vi->setText($_SESSION[$key]);
unset($_SESSION[$key]);
return $vi->verifyInput($input);
self::log("Session saved vcode is: $text");
} else if (!isset($_COOKIE['hvimage']) || empty($_COOKIE['hvimage'])) {
self::log("No cookie set (hvimage)");
setcookie("hvimage", '', time());
return false;
} else if ($_COOKIE['hvimage'] == sha1(strtoupper(trim($input)))) {
self::log("Cookie validation OK (hvimage)");
return true;
} else {
self::log("No alternative session key saved ($key)");
}
return false;
}
/**
* Constructor
* @param string $text
*/
public function __construct($text='') {
if (!empty($text))
$this->setText(trim($text));
}
/**
* Returns the image width
* @return int
*/
public function getWidth() {
return $this->width;
}
/**
* Sets the image width
* @param int $width
*/
public function setWidth($width) {
if (!is_numeric($width)) {
throw new Exception("Invalid image width: '$width'");
} else {
$width = intval($width);
if ($width < 10 || $width > 500)
$width = 0;
$this->width = $width;
}
}
/**
* Returns the image height
* @return int
*/
public function getHeight() {
return $this->height;
}
/**
* Sets the image height
* @param int $height
*/
public function setHeight($height) {
if (!is_numeric($height)) {
throw new Exception("Invalid image height: '$height'");
} else if (intval($height) < 20) {
throw new Exception("Invalid image height: '$height' (min width is 20)");
} else {
$this->height = intval($height);
}
}
/**
* Returns the image text
* @return string
*/
public function getText() {
if (empty($this->text)) {
$this->setRandomText();
}
return $this->text;
}
/**
* Sets the text to display
* @param string $text
*/
public function setText($text) {
if (!is_scalar($text)) {
throw new Exception("Invalid image text (must be a scalar value)");
} else if (empty($text) || strlen(trim($text)) == 0) {
$this->text = '';
} else {
$this->text = strval($text);
}
}
/**
* Returns the noise factor
* @return double
*/
public function getNoise() {
return $this->noise;
}
/**
* Sets the noise factor
* @param double $noise
*/
public function setNoise($noise) {
if (!is_numeric($noise)) {
throw new Exception('Invalid image noise (not numeric)');
} else if ($noise > 1 || $noise < 0) {
throw new Exception('Invalid image noise (must be between 0 and 1)');
} else {
$this->noise = $noise;
}
}
/**
* Sets a random text
* @param int $size
* @param string charset
*/
public function setRandomText($size=8, $charset='abcdefghkmnpqrtwxyzABCDEFGHIJKLMNPRTUVWXYZ2346789') {
$charset = preg_replace('/[\s]/', '', $charset);
if (!is_numeric($size)) {
throw new Exception("'Invalid random text size ('$size')'");
} else if (empty($charset)) {
throw new Exception('Invalid character set for random text (empty string)');
} else {
$l = strlen($charset) - 1;
$o = '';
while (strlen($o) < $size) {
$o .= substr($charset, rand(0, $l), 1);
}
$this->text = $o;
return $o;
}
}
/**
* Generates the png image, returns the local file path
* @return string
*/
public function generateImage() {
$yPositionRange = 2;
$fontSize = $this->getHeight() - (2 * $yPositionRange);
$charWidth = imagefontwidth($fontSize) + 3;
$charHeight = imagefontheight($fontSize);
$textWidth = $charWidth * strlen($this->getText());
if ($this->getWidth() == 0)
$this->width = $textWidth + (2 * $yPositionRange);
$this->width = 10 * ceil($this->width / 10);
$noisePixels = $this->getWidth() * $this->getHeight() * $this->getNoise();
$x0 = ceil(($this->getWidth() - $textWidth) / 2);
$y0 = ceil(($this->getHeight() - $charHeight) / 2);
$img = imagecreatetruecolor($this->getWidth(), $this->getHeight());
imagefill($img, 0, 0, 0xffffff);
// Noise
$w = $this->getWidth() - 1;
$h = $this->getHeight() - 1;
for ($i = 0; $i < $noisePixels; $i++) {
$c = 0x000000;
imagesetpixel($img, rand(0, $w), rand(0, $h), $c);
}
$text = $this->getText();
for ($i = 0; $i < strlen($text); $i++) {
$ch = substr($text, $i, 1);
imagestring($img, $fontSize, $x0 + $i * $charWidth, $y0, $ch, 0x000000);
}
for ($i = floor($noisePixels); $i < $noisePixels; $i++) {
$c = 0xffffff;
imagesetpixel($img, rand(0, $w), rand(0, $h), $c);
}
$file = FileSystem::getTempFileName();
imagepng($img, $file);
imagedestroy($img);
return $file;
}
/**
* Sends an image as PNG, flushes the output buffer.
*/
public function sendImage() {
$file = $this->generateImage();
if (!empty($file)) {
header('Content-type: image/png');
header('Content-Length: ' . filesize($file));
header('Cache-Control: max-age:0, no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
header('Pragma: no-cache');
header('Connection:Close');
while(ob_get_level()) ob_get_clean();
print file_get_contents($file);
@unlink($file);
}
}
/**
* Checks the input against the $text instance variable. One spelling
* error is accepted for characters like O<-->0 and S<-->5.
* @param string $input
* @return bool
*/
public function verifyInput($input) {
$input = strtoupper(trim($input, " \n\r\t"));
$text = strtoupper(trim($this->getText(), " \n\r\t"));
if (empty($input)) {
self::log('Verification failed: No input');
return false;
} else if (strlen($input) != strlen($text)) {
self::log('Verification failed: Text lengths differ');
return false;
} else if ($input == $text) {
self::log('Verification OK: Texts are equal');
return true;
} else if (levenshtein($input, $text) > 1) {
self::log('Verification failed: More than one character difference');
return false;
} else {
$t1 = $text;
$t2 = $input;
$t1 = str_replace(array('0', 'Q', '5'), array('O', 'O', 'S'), $t1);
$t2 = str_replace(array('0', 'Q', '5'), array('O', 'O', 'S'), $t2);
if ($t1 == $t2) {
self::log('Verification OK, after compensation of optical redundant characters');
return true;
} else {
self::log('Verification failed: Even after correction of optical redundant characters');
return false;
}
}
return false;
}
}