Last active
December 22, 2021 08:39
-
-
Save ve3/920c6cd179e0cb9831c0fd1f420c6006 to your computer and use it in GitHub Desktop.
Spambot prevention
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
/** | |
* Antibot JS class. | |
*/ | |
class Antibot { | |
/** | |
* Setup honeypot form group. | |
* | |
* @param {string} selector | |
*/ | |
setupHoneypotFormGroup(selector) { | |
let targetElement = document.querySelector(selector); | |
if (typeof(targetElement) !== 'object' || null === targetElement || '' === targetElement) { | |
console.error('The target honeypot form group is not exists.'); | |
throw 'The target honeypot form group is not exists.'; | |
} | |
targetElement.style.setProperty('display', 'none', 'important'); | |
}// setupHoneypotFormGroup | |
} |
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 | |
/** | |
* Antibot (anti spambot). | |
* | |
* @license MIT https://opensource.org/licenses/MIT | |
*/ | |
namespace Rundiz\Antibot; | |
/** | |
* Anti spambot class. | |
* | |
* @property-read array $errors. The error messages and codes. Format is<pre> | |
* array( | |
* 0 => array( | |
* 'code' => \Rundiz\Antibot\Antibot::NOTAUTHORIZE_FAILED_SETCOOKIE, | |
* 'message' => 'Failed to set cookie.', | |
* ), | |
* 1 => array(...), | |
* ) | |
* </pre> | |
* @property-read string $honeypotSessionName The honeypot session name that contain the name of honeypot field. | |
*/ | |
class Antibot | |
{ | |
/** | |
* Failed to validate honeypot. (Failed to validate human.) | |
*/ | |
const NOTAUTHORIZE_FAILED_HONEYPOT = 2; | |
/** | |
* Failed to set, get cookie. | |
*/ | |
const NOTAUTHORIZE_FAILED_SETCOOKIE = 1; | |
/** | |
* @var array Errors. Format is<pre> | |
* array( | |
* 0 => array( | |
* 'code' => \Rundiz\Antibot\Antibot::NOTAUTHORIZE_FAILED_SETCOOKIE, | |
* 'message' => 'Failed to set cookie.', | |
* ), | |
* 1 => array(...), | |
* ) | |
* </pre> | |
*/ | |
protected $errors = []; | |
/** | |
* @var array The honeypot names. | |
*/ | |
protected $honeypotNames = ['birthdate', 'email', 'fullname', 'phonenumber', 'mobilenumber', 'secondary-email', 'national-id']; | |
/** | |
* @var string The session name for honeypot that will be use in the form. Example: `$_SESSION[$Antibot->$honeypotSessionName]` will be one of `honeypotNames` value. | |
*/ | |
protected $honeypotSessionName = 'rundizAntibotHoneypotName'; | |
/** | |
* Anti spambot class. | |
*/ | |
public function __construct() | |
{ | |
}// __construct | |
/** | |
* Magic get. | |
* | |
* @param string $name | |
* @return void | |
*/ | |
public function __get($name) | |
{ | |
if (property_exists($this, $name)) { | |
return $this->{$name}; | |
} | |
}// __get | |
/** | |
* Get the honeypot name. | |
* | |
* @return string | |
* @throws \LogicException Throw the errors if session not started. | |
*/ | |
public function getHoneypotName() | |
{ | |
if (session_status() !== PHP_SESSION_ACTIVE) { | |
throw new \LogicException('Please start PHP session before call to this method.'); | |
} | |
if (isset($_SESSION[$this->honeypotSessionName])) { | |
return $_SESSION[$this->honeypotSessionName]; | |
} | |
return ''; | |
}// getName | |
/** | |
* Set and check cookie. | |
* | |
* @param string $cookieName The cookie name. | |
* @param string $paramName The querystring name that will be use after set cookie and redirected. | |
* @return bool Return `true` on success, `false` on failure. Access `errors` property to see details. | |
*/ | |
public function setAndCheckCookie(string $cookieName = 'rundiz-antibot-cookie-test', string $paramName = 'rda-setcookie'): bool | |
{ | |
if (!$this->testCookie($cookieName)) { | |
// if cookie is not set or not matched value. | |
// set a cookie. | |
$domain = ($_SERVER['HTTP_HOST'] ?? 'localhost'); | |
setcookie($cookieName, '1', time()+60*60*24*1, '/', $domain, false, true); | |
unset($domain); | |
// check that redirected. | |
if (isset($_GET[$paramName]) && '1' === $_GET[$paramName]) { | |
// if redirected and querystring value is matched. (cookie was set but not found and redirected.) | |
http_response_code(400); | |
$this->errors[] = [ | |
'code' => static::NOTAUTHORIZE_FAILED_SETCOOKIE, | |
'message' => 'Failed to set cookie.', | |
]; | |
return false; | |
} else { | |
// if not redirected or querystring value is not matched. | |
// try to redirect. | |
$http = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . '://'; | |
$domain = ($_SERVER['HTTP_HOST'] ?? 'localhost'); | |
$path = ($_SERVER['REQUEST_URI'] ?? '');// included /path/file.php?param=value | |
$currentURL = $http . $domain . $path; | |
unset($domain, $http, $path); | |
$parsedURL = parse_url($currentURL); | |
$query = ($parsedURL['query'] ?? ''); | |
parse_str($query, $parsedQuery); | |
unset($parsedURL, $query); | |
if (!isset($parsedQuery[$paramName]) || '1' !== $parsedQuery[$paramName]) { | |
if (strpos($currentURL, '?') === false) { | |
$currentURL .= '?'; | |
} else { | |
$currentURL .= '&'; | |
} | |
$currentURL .= rawurlencode($paramName) . '=1'; | |
} | |
unset($parsedQuery); | |
header('Location: ' . $currentURL); | |
exit(); | |
} | |
} | |
return true; | |
}// setAndCheckCookie | |
/** | |
* Set and get the field name for use in honeypot. To get only, use `getHoneypotName()` method. | |
* | |
* @param array $allowedNames The custom allowed names. If leave empty, it will be use default. | |
* @return string Return generated honeypot name to use. The honeypot name will be set to session `$_SESSION[$Antibot->honeypotSessionName]`. Use this session to check that honeypot name must be empty. | |
* @throws \LogicException Throw the errors if session not started. | |
*/ | |
public function setAndGetHoneypotName(array $allowedNames = []) | |
{ | |
if (session_status() !== PHP_SESSION_ACTIVE) { | |
throw new \LogicException('Please start PHP session before call to this method.'); | |
} | |
if (!empty($allowedNames)) { | |
$this->honeypotNames = $allowedNames; | |
} | |
$output = $this->honeypotNames[mt_rand(0, (count($this->honeypotNames) - 1))] . '_' . mt_rand(0, 999); | |
$_SESSION[$this->honeypotSessionName] = $output; | |
return $output; | |
}// setAndGetNames | |
/** | |
* Test antibot cookie that must exists and its value is 1. | |
* | |
* @param string $cookieName | |
* @return bool Return `true` if success, `false` for failure. | |
*/ | |
public function testCookie(string $cookieName = 'rundiz-antibot-cookie-test'): bool | |
{ | |
return (isset($_COOKIE[$cookieName]) && '1' === $_COOKIE[$cookieName]); | |
}// testCookie | |
/** | |
* Test honeypot field. | |
* | |
* This test will be work on method GET or POST. To use with other method, please access `honeypotSessionName` property and manually check the input. | |
* | |
* @param int $input The input type must be one of `INPUT_GET`, `INPUT_POST`. | |
* @return bool Return `true` on success, `false` on failure. Access `errors` property to see error details. | |
*/ | |
public function testHoneypot($input): bool | |
{ | |
if (INPUT_GET !== $input && INPUT_POST !== $input) { | |
throw new \InvalidArgumentException('The $input argument must be one of INPUT_GET or INPUT_POST constant.'); | |
} | |
$honeypotValue = filter_input($input, $_SESSION[$this->honeypotSessionName]); | |
if ('' !== $honeypotValue || is_null($honeypotValue)) { | |
// if honeypot field is not exists or bot filled that form. | |
$this->errors[] = [ | |
'code' => static::NOTAUTHORIZE_FAILED_HONEYPOT, | |
'message' => 'Failed to validate human.', | |
]; | |
return false; | |
} | |
return true; | |
}// testHoneypot | |
/** | |
* Unset cookie. | |
* | |
* @param string $cookieName | |
* @return bool Return result from `setcookie()` function. | |
*/ | |
public function unsetCookie(string $cookieName = 'rundiz-antibot-cookie-test'): bool | |
{ | |
$domain = ($_SERVER['HTTP_HOST'] ?? 'localhost'); | |
$result = setcookie($cookieName, '', time()-60*60*24*1, '/', $domain, false, true); | |
setcookie($cookieName, '', time()-60*60*24*1, '/', $domain, false, false); | |
setcookie($cookieName, '', time()-60*60*24*1, '/', '', false, false); | |
unset($domain); | |
unset($_COOKIE[$cookieName]); | |
return $result; | |
}// unsetCookie | |
} |
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 | |
require_once 'Antibot.php'; | |
session_start(); | |
if (isset($_SERVER['REQUEST_METHOD']) && 'post' === strtolower($_SERVER['REQUEST_METHOD'])) { | |
// if method POST. | |
$Antibot = new \Rundiz\Antibot\Antibot(); | |
// use class to test result. | |
$result1 = $Antibot->testHoneypot(INPUT_POST); | |
// use custom test. | |
$honeypotValue = ($_POST[$_SESSION[$Antibot->honeypotSessionName]] ?? null); | |
$result2 = '' === $honeypotValue && !is_null($honeypotValue); | |
unset($honeypotValue); | |
if (!$result2) { | |
$errorString = '<p>Failed to validate human.</p>' . PHP_EOL; | |
} | |
if ($Antibot->errors && is_array($Antibot->errors) && !empty($Antibot->errors)) { | |
$errorString = ''; | |
foreach ($Antibot->errors as $error) { | |
$errorString .= '<p>' . $error['message'] . '</p>' . PHP_EOL; | |
} | |
unset($error); | |
echo $errorString . PHP_EOL; | |
} | |
if (true === $result1 && true === $result2) { | |
$successString = '<p>Human verification passed.</p>' . PHP_EOL; | |
} | |
unset($Antibot); | |
} | |
?> | |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Spambot prevention.</title> | |
<link rel="stylesheet" href="style.css"> | |
</head> | |
<body> | |
<main class="container"> | |
<div id="page-contents"> | |
<h1>Spambot prevention.</h1> | |
<?php | |
if (isset($errorString)) { | |
echo '<div class="alert-error">' . $errorString . '</div>' . PHP_EOL; | |
} | |
if (isset($successString)) { | |
echo '<div class="alert-success">' . $successString . '</div>' . PHP_EOL; | |
} | |
echo '<p>Honeypot test use class: <code>' . var_export($result1, true) . '</code></p>' . PHP_EOL; | |
echo '<p>Honeypot test use custom: <code>' . var_export($result2, true) . '</code></p>' . PHP_EOL; | |
?> | |
<h2>Form result</h2> | |
<p><strong>Name: </strong><?php echo htmlspecialchars(filter_input(INPUT_POST, 'name'), ENT_QUOTES); ?></p> | |
<p><strong>Message: </strong><?php echo htmlspecialchars(filter_input(INPUT_POST, 'message'), ENT_QUOTES); ?></p> | |
<p> | |
<a href="form.php">Go back to form</a> | |
<a href="clear-session-cookie.php">Clear sessions & cookies</a> | |
</p> | |
<h3>Debug info</h3> | |
<pre>$_POST: <?php echo htmlspecialchars(print_r($_POST, true), ENT_QUOTES); ?> | |
$_SESSION: <?php echo htmlspecialchars(print_r($_SESSION, true), ENT_QUOTES); ?> | |
User agent: <?php echo htmlspecialchars(print_r($_SERVER['HTTP_USER_AGENT'], true), ENT_QUOTES); ?> | |
Request time: <?php echo date('Y-m-d H:i:s'); ?> | |
</pre> | |
</div> | |
<footer id="page-footer"> | |
Created by <a href="https://rundiz.com" target="rundiz">rundiz.com</a> | |
| | |
<a href="https://gist.github.com/ve3/920c6cd179e0cb9831c0fd1f420c6006" target="gist">View source code</a> | |
</footer> | |
</main> | |
</body> | |
</html> |
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 | |
use Rundiz\Antibot\Antibot; | |
require_once 'Antibot.php'; | |
session_start(); | |
$Antibot = new \Rundiz\Antibot\Antibot(); | |
if (false === $Antibot->setAndCheckCookie()) { | |
$errorString = ''; | |
foreach ($Antibot->errors as $error) { | |
$errorString .= '<p>' . $error['message'] . '</p>' . PHP_EOL; | |
} | |
unset($error); | |
} | |
if (!isset($errorString)) { | |
$honeypotName = $Antibot->setAndGetHoneypotName(); | |
} | |
unset($Antibot); | |
?> | |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Spambot prevention.</title> | |
<link rel="stylesheet" href="style.css"> | |
</head> | |
<body> | |
<main class="container"> | |
<div id="page-contents"> | |
<h1>Spambot prevention.</h1> | |
<p>Submit the form below to test.</p> | |
<?php | |
if (isset($errorString)) { | |
echo '<div class="alert-error">' . $errorString . '</div>' . PHP_EOL; | |
} | |
?> | |
<form method="post" action="form-process.php"> | |
<div class="form-group"> | |
<label for="name">Name:</label> | |
<input id="name" type="text" name="name" required> | |
</div> | |
<div class="form-group"> | |
<label for="message">Message:</label> | |
<textarea id="message" name="message"></textarea> | |
</div> | |
<?php if (isset($honeypotName)) { ?> | |
<div id="rda-form-group" class="form-group"> | |
<label for="<?php echo $honeypotName; ?>">Info</label> | |
<input id="<?php echo $honeypotName; ?>" type="text" name="<?php echo $honeypotName; ?>"> | |
</div> | |
<?php }// endif; ?> | |
<button type="submit">Submit</button> | |
</form> | |
<p><a href="clear-session-cookie.php">Clear sessions & cookies</a></p> | |
</div> | |
<footer id="page-footer"> | |
Created by <a href="https://rundiz.com" target="rundiz">rundiz.com</a> | |
| | |
<a href="https://gist.github.com/ve3/920c6cd179e0cb9831c0fd1f420c6006" target="gist">View source code</a> | |
</footer> | |
</main> | |
<script type="application/javascript" src="Antibot.js"></script> | |
<script type="application/javascript"> | |
document.addEventListener('DOMContentLoaded', () => { | |
let antibot = new Antibot(); | |
antibot.setupHoneypotFormGroup('#rda-form-group'); | |
}); | |
</script> | |
</body> | |
</html> |
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
* { | |
box-sizing: border-box; | |
} | |
html,body { | |
background-color: #fff; | |
height: 100vh; | |
} | |
body { | |
color: #333; | |
font-family: Verdana, Arial, Helvetica, sans-serif; | |
font-size: 16px; | |
margin: 0; | |
padding: 0; | |
} | |
form { | |
margin-bottom: 20px; | |
} | |
button { | |
background-color: #eee; | |
cursor: pointer; | |
} | |
button:hover { | |
background-color: #f5f5f5; | |
} | |
button:active { | |
background-color: #ddd; | |
} | |
button, | |
input, | |
textarea { | |
border: 1px solid #ccc; | |
border-radius: 5px; | |
font-size: 1rem; | |
padding: 5px 8px; | |
} | |
code { | |
background-color: #eee; | |
padding: 2px 5px; | |
} | |
pre { | |
background-color: #eee; | |
padding: 10px; | |
} | |
.alert-error { | |
background-color: red; | |
color: white; | |
margin-bottom: 20px; | |
padding: 1px 10px; | |
} | |
.alert-success { | |
background-color: green; | |
color: white; | |
margin-bottom: 20px; | |
padding: 1px 10px; | |
} | |
.container { | |
display: grid; | |
grid-template-rows: 1fr auto; | |
min-height: 100vh; | |
} | |
#page-contents { | |
margin-bottom: 20px; | |
} | |
#page-contents, | |
#page-footer { | |
margin-left: auto; | |
margin-right: auto; | |
max-width: 80vw; | |
width: 100%; | |
} | |
@media (min-width: 1200px) { | |
#page-contents, | |
#page-footer { | |
max-width: 1000px; | |
} | |
} | |
#page-footer { | |
border-top: 1px solid #ccc; | |
line-height: 30px; | |
min-height: 30px; | |
} | |
.form-group { | |
margin-bottom: 10px; | |
} | |
.form-group label { | |
display: block; | |
} | |
#message { | |
height: 100px; | |
resize: vertical; | |
width: 100%; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment