A calculation-based anti-spam for web forms, written in PHP.
The documentation is available under the first file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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]; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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