A calculation-based anti-spam for web forms, written in PHP. The documentation is available under the first file.
<?php | |
if(session_id() == '') { | |
session_start(); | |
} | |
/** | |
* This class generates a calculation to prevent the spam on forms. | |
* | |
* @author Amaury Carrade | |
*/ | |
/** | |
* Copyright 2013 Amaury Carrade. | |
* | |
* This software is provided 'as-is', without any express or implied warranty. In no event will the | |
* authors be held liable for any damages arising from the use of this software. | |
* | |
* Permission is granted to anyone to use this software for any purpose, including commercial | |
* applications, and to alter it and redistribute it freely, subject to the following restrictions: | |
* | |
* 1. The origin of this software must not be misrepresented; you must not claim that you wrote the | |
* original software. If you use this software in a product, an acknowledgment in the product | |
* documentation would be appreciated but is not required. | |
* 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being | |
* the original software. | |
* 3. This notice may not be removed or altered from any source distribution. | |
*/ | |
class CalculusAntiSpam { | |
const DIFFICULTY_EASY = 1; | |
const DIFFICULTY_MEDIUM = 2; | |
const DIFFICULTY_HARD = 3; | |
static $defaults = array(); | |
static $config = array(); // Needs to be static because the method that sets this is a static method. | |
/** | |
* Initialize the default values. | |
*/ | |
static protected function initDefaults() { | |
if(self::$defaults != array()) return; | |
$numbers = array('', 'un', 'deux', 'trois', 'quatre', 'cinq', 'six', 'sept', 'huit', 'neuf', 'dix', 'onze', 'douze', 'treize', 'quatorze', 'quinze', 'seize', 'dix-sept', 'dix-huit', 'dix-neuf', 'vingt', 'vingt-et-un'); | |
self::$defaults = array('difficulty' => self::DIFFICULTY_MEDIUM, | |
'sessionNamespace' => 'CalculusAntiSpam', | |
'numbers' => $numbers, | |
); | |
} | |
/** | |
* Saves a configuration in the class (for defaults) or in the object (for current config). | |
* | |
* @param array $config | |
* @param string $target "default" or "current". | |
* @see self::setDefaults for explanations on $config. | |
*/ | |
static protected function setInternalConfig(array $config, $target) { | |
self::initDefaults(); | |
$newConfig = self::$defaults; // Config is always based on the default config. | |
foreach ($config as $key => $value) { | |
switch ($key) { | |
case 'difficulty': | |
if(in_array($value, array(self::DIFFICULTY_EASY, | |
self::DIFFICULTY_MEDIUM, | |
self::DIFFICULTY_HARD))) { | |
$newConfig['difficulty'] = $value; | |
} | |
break; | |
case 'sessionNamespace': | |
$newConfig['sessionNamespace'] = (string) $value; | |
break; | |
case 'numbers': | |
if(is_array($value) AND count($value) == 22) { | |
$newConfig['numbers'] = $value; | |
} | |
break; | |
case 'logfile': | |
$newConfig['logfile'] == $value; | |
} | |
} | |
if($target == 'default') { | |
self::$defaults = $newConfig; | |
} | |
else { | |
self::$config = $newConfig; | |
} | |
} | |
/** | |
* Sets the default configuration. | |
* | |
* Allowed options are: | |
* | |
* - difficulty: self::DIFFICULTY_EASY, self::DIFFICULTY_MEDIUM or self::DIFFICULTY_HARD | |
* (see constructor for explanations on difficulty levels); | |
* | |
* - sessionNamespace: the session used will be $_SESSION[sessionNamespace]; | |
* | |
* - numbers: an array with the first item empty, and the following items filled with the | |
* numbers from one to twenty one. | |
* Ex: array('', 'one', 'two', ..., 'twenty one'). | |
* Defaults: numbers in french. | |
* | |
* - logfile: if you want to be logged the fails, specify here a file. If not, sets to NULL. | |
* The file must be writable. If it is not, no error will be thrown, and obviously, the log | |
* will not be saved. | |
* The log includes the timestamp, the IP, the calculation, the answer, the difficulty level | |
* and the test ID (see self::generate() for this one). | |
* » Tip: you can use fail2ban with this log. | |
* | |
* @param array $defaults | |
*/ | |
static public function setDefaults(array $defaults) { | |
self::setInternalConfig($defaults, 'default'); | |
} | |
/** | |
* Sets the current configuration. | |
* | |
* @see self::setDefaults() for allowed options. | |
* @param array $config | |
*/ | |
public function setConfig(array $config) { | |
self::setInternalConfig($config, 'current'); | |
} | |
/** | |
* Initialize the object. | |
* | |
* | |
* Difficulty explained | |
* -------------------- | |
* | |
* - EASY: the numbers are between 1 and 8 (between 2 and 8 if multiplication); | |
* the numbers are ordered, the highest is the first in the operation; | |
* no negative numbers. | |
* | |
* - MEDIUM: the numbers are between 1 and 12 (between 2 and 10 if multiplication); | |
* the first number can be a negative one; | |
* the numbers are not ordered. | |
* | |
* - HARD: the numbers are between 1 and 21 (between 2 and 12 if multiplication); | |
* the numbers are not ordered; | |
* the second number can be a negative one too. | |
* | |
* | |
* @param integer $difficulty The difficulty level: self::DIFFICULTY_EASY, | |
* self::DIFFICULTY_MEDIUM or | |
* self::DIFFICULTY_HARD (see below). | |
* Default: self::DIFFICULTY_MEDIUM (or the default option defined with self::setDefaults()). | |
* | |
* @param string $sessionNamespace The session's namespace. Session will be stored in | |
* $_SESSION[$sessionNamespace]. | |
* Default: "CalculusAntiSpam" (or the default option defined with self::setDefaults()). | |
* | |
* @param array $numbers An array with the first item empty, and the following items filled | |
* with the numbers from one to twenty one. | |
* Ex: array('', 'one', 'two', ..., 'twenty one'). | |
* Defaults: numbers in french. | |
* | |
* @return void. | |
*/ | |
public function __construct($difficulty = NULL, $numbers = NULL, $sessionNamespace = NULL) { | |
self::setInternalConfig(self::$defaults, 'current'); // Import config from defaults. | |
$userConfig = array(); | |
if($difficulty != NULL) { | |
$userConfig['difficulty'] = $difficulty; | |
} | |
if($sessionNamespace != NULL) { | |
$userConfig['sessionNamespace'] = $sessionNamespace; | |
} | |
if($numbers != NULL) { | |
$userConfig['numbers'] = $numbers; | |
} | |
self::setInternalConfig($userConfig, 'current'); | |
} | |
/** | |
* Generates the question and saves the answer to the session. | |
* | |
* @param string $testId An unique identifier. If you have more than one anti-spam in one page, use this to | |
* differentiate the different ones. If not, ignore this. | |
* Default: 'noSpam1'. | |
* @return string A string containing the calcul, written with the numbers saved in configuration. | |
*/ | |
public function generate($testId = 'noSpam1') { | |
$numbers = self::$config['numbers']; | |
$operators = array(0 => '+', | |
1 => '−', | |
2 => '×'); | |
// Lower numbers for lower difficulties. | |
if(self::$config['difficulty'] == self::DIFFICULTY_MEDIUM) { | |
$numbers = array_slice($numbers, 0, 13); // From 1 to 12. | |
} | |
else if(self::$config['difficulty'] == self::DIFFICULTY_EASY) { | |
$numbers = array_slice($numbers, 0, 9); // From 1 to 8. | |
} | |
// Generates the operator | |
$operator = mt_rand(0, 2); | |
if($operator == 2) { // An easier calcul for the multiplication. | |
if(self::$config['difficulty'] <= self::DIFFICULTY_MEDIUM) { | |
// Numbers between one and ten only. | |
$numbers = array_slice($numbers, 0, 11); | |
} | |
else { | |
// Numbers between one and twelve only. | |
$numbers = array_slice($numbers, 0, 13); | |
} | |
} | |
// Generates two random numbers | |
if($operator != 2) { | |
$number1 = mt_rand(1, count($numbers) - 1); // Zero is excluded. | |
$number2 = mt_rand(1, count($numbers) - 1); | |
} | |
else { | |
$number1 = mt_rand(2, count($numbers) - 1); // Zero is excluded. Same for one (because it's a multiplication). | |
$number2 = mt_rand(2, count($numbers) - 1); | |
} | |
if(self::$config['difficulty'] == self::DIFFICULTY_EASY) { | |
// First number is greater than second. | |
if($number1 < $number2) { | |
// Intervert. | |
$number1 = $number1 + $number2; | |
$number2 = $number1 - $number2; | |
$number1 = $number1 - $number2; | |
} | |
} | |
$number1signText = $number2signText = NULL; // Used for the “−” sign before the text. | |
$number2TextBefore = $number2TextAfter = NULL; // Used for the parenthesis. | |
if(self::$config['difficulty'] >= self::DIFFICULTY_MEDIUM) { | |
// Random sign for the numbers | |
$number1sign = mt_rand(0, 1); | |
if($number1sign) { | |
$number1 = -$number1; | |
$number1signText = '− '; | |
} | |
if(self::$config['difficulty'] == self::DIFFICULTY_HARD) { | |
$number2sign = mt_rand(0, 1); | |
if($number2sign) { | |
$number2 = -$number2; | |
$number2TextBefore = '('; | |
$number2signText = '− '; | |
$number2TextAfter = ')'; | |
} | |
} | |
} | |
// Does the calcul | |
$result = 0; | |
switch ($operator) { | |
case 0: | |
$result = $number1 + $number2; | |
break; | |
case 1: | |
$result = $number1 - $number2; | |
break; | |
case 2: | |
$result = $number1 * $number2; | |
break; | |
} | |
$text = $number1signText . $numbers[abs($number1)] . ' ' . $operators[$operator] . ' ' . $number2TextBefore . $number2signText . $numbers[abs($number2)] . $number2TextAfter; | |
$numbersText = str_replace('-', '−', $number1) . ' ' . $operators[$operator] . ' ' . trim($number2TextBefore) . str_replace('-', '−', $number2) . $number2TextAfter; | |
// Trim : “(−12)”, not “(− 12)”. | |
$_SESSION[self::$config['sessionNamespace']][$testId] = array('text' => $text, | |
'result' => $result, | |
'resultText' => str_replace('-', '−', $result), | |
'numbersText' => $numbersText, | |
'difficulty' => self::$config['difficulty'], | |
); | |
return $text; | |
} | |
/** | |
* Checks if the form wasn't submitted by a robot. | |
* | |
* @param integer $userInput | |
* @param string $testId See self::generate(). | |
* | |
* @return bool true if the form wasn't submitted by a robot. False else, | |
* or if the answer was not found in the session. | |
*/ | |
public function check($userInput, $testId = 'noSpam1') { | |
if(!isset($_SESSION[self::$config['sessionNamespace']][$testId]['result'])) { | |
return false; | |
} | |
$userCalcul = (int) $userInput; | |
$success = ($userCalcul == (int) $_SESSION[self::$config['sessionNamespace']][$testId]['result']); | |
if(!$success && self::$config['logfile'] != NULL | |
&& is_readable(self::$config['logfile']) | |
&& is_writable(self::$config['logfile'])) { // Log. | |
$date = new DateTime(); | |
$difficulty = 'easy'; | |
if($_SESSION[self::$config['sessionNamespace']][$testId]['difficulty'] == self::DIFFICULTY_MEDIUM) { | |
$difficulty = 'medium'; | |
} | |
else if($_SESSION[self::$config['sessionNamespace']][$testId]['difficulty'] == self::DIFFICULTY_HARD) { | |
$difficulty = 'hard'; | |
} | |
$log = file_get_contents(self::$config['logfile']); | |
$log .= '[' . $date->format('Y-m-d H:i:s') . '] Spam check error from ' . $_SERVER['REMOTE_ADDR'] . ' with ' . $_SESSION[SNS]['spamCheck']['numbersText'] . ' = ' . $userCalcul . ' (correct answer is ' . $_SESSION[SNS]['spamCheck']['resultText'] . ') (difficulty: ' . $difficulty . ') (test id:' . $testId . ').' . "\n"; | |
$log = str_replace(array('×', '−'), array('*', '-'), $log); // Human-readable log in ASCII. | |
file_put_contents(self::$config['logfile'], $log); | |
} | |
return $success; | |
} | |
/** | |
* Returns some informations about the answer of the user. | |
* | |
* @param string $testId See self::generate(). | |
* | |
* @return array An array with the following keys: | |
* - 'result' -> the numeric result of the calcul | |
* - 'resultText' -> a typographically-correct result | |
* - 'text' -> the text (with words) | |
* - 'numbersText' -> the text (with typographically-correct numbers) | |
* - 'difficulty' -> the difficulty | |
*/ | |
public function getAnswer($testId = 'noSpam1') { | |
if(!isset($_SESSION[self::$config['sessionNamespace']][$testId])) { | |
return array(); | |
} | |
return $_SESSION[self::$config['sessionNamespace']][$testId]; | |
} | |
} |
<?php | |
/** | |
* The class is auto-documented, following PHPDoc (or JavaDoc) rules. | |
*/ | |
require_once('path/to/CalculusAntiSpam.php'); | |
header('Content-Type: text/plain'); | |
// Set defaults. | |
/** | |
* The PHPDocumentation says: | |
* | |
* Allowed options are: | |
* | |
* - difficulty: self::DIFFICULTY_EASY, self::DIFFICULTY_MEDIUM or self::DIFFICULTY_HARD | |
* (see constructor for explanations on difficulty levels); | |
* | |
* - sessionNamespace: the session used will be $_SESSION[sessionNamespace]; | |
* | |
* - numbers: an array with the first item empty, and the following items filled with the | |
* numbers from one to twenty one. | |
* Ex: array('', 'one', 'two', ..., 'twenty one'). | |
* Defaults: numbers in french. | |
* | |
* - logfile: if you want to be logged the fails, specify here a file. If not, sets to NULL. | |
* The file must be writable. If it is not, no error will be thrown, and obviously, the log | |
* will not be saved. | |
* The log includes the timestamp, the IP, the calculation, the answer, the difficulty level | |
* and the test ID (see self::generate() for this one). | |
* » Tip: you can use fail2ban with this log. | |
*/ | |
CalculusAntiSpam::setDefaults(array( | |
'difficulty' => CalculusAntiSpam::DIFFICULTY_EASY, | |
'logfile' => 'path/to/antispam.log' | |
) | |
); | |
# | |
# In the form | |
# ************************ | |
$nospam = new CalculusAntiSpam(); | |
// Generates a new calculation. | |
// This method returns a string like “One + five” or “Eight × nine”, according to the configuration for the language used. | |
echo $this->generate(); | |
// If you want more than one calculation | |
echo $this->generate('anUniqueKey'); | |
echo $this->generate('anOtherKey'); | |
// If you want to change the configuration (ex between two calculations) | |
$nospam->setConfig(array('difficulty' => CalculusAntiSpam::DIFFICULTY_HARD)); | |
# | |
# After the form | |
# ************************ | |
// If you want to check the result. | |
if($noSpam->check($userAnswer)) { | |
// Your code... | |
} | |
// With more than one calculation | |
if($noSpam->check($userAnswer, 'anUniqueKey')) { | |
// Your code... | |
} | |
// If you want to get some informations about the question and the answer of the user (see doc in file) | |
var_dump( $nospam->getAnswer() ); | |
var_dump( $nospam->getAnswer('anOtherKey') ); | |
/** | |
* The PHPDocumentation says: | |
* | |
* @return array An array with the following keys: | |
* - 'result' -> the numeric result of the calcul | |
* - 'resultText' -> a typographically-correct result | |
* - 'text' -> the text (with words) | |
* - 'numbersText' -> the text (with typographically-correct numbers) | |
* - 'difficulty' -> the difficulty | |
*/ | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment