Skip to content

Instantly share code, notes, and snippets.

@ve3
Last active December 22, 2021 08:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ve3/920c6cd179e0cb9831c0fd1f420c6006 to your computer and use it in GitHub Desktop.
Save ve3/920c6cd179e0cb9831c0fd1f420c6006 to your computer and use it in GitHub Desktop.
Spambot prevention
/**
* 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
}
<?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
}
<?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 &amp; 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>
<?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 &amp; 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>
* {
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