Skip to content

Commit

Permalink
feat: Add JS honeypot
Browse files Browse the repository at this point in the history
  • Loading branch information
johnnynotsolucky committed Nov 28, 2024
1 parent 017f998 commit ea850c3
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 20 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Release Notes for honeypot

## 1.0.0-beta
## 1.0.0@beta

- Initial beta release
36 changes: 30 additions & 6 deletions src/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,24 @@
class Plugin extends BasePlugin
{
/**
* @var string[]
* @var int
*/
private const LOG_LEVELS = ['debug', 'info', 'error', 'warning'];
public const DEFAULT_TIMETRAP_TIMEOUT = 2000;

/**
* @var int
*/
private const DEFAULT_TIMETRAP_TIMEOUT = 2000;
public const DEFAULT_JS_TIMEOUT = 2000;

/**
* @var string
*/
public const DEFAULT_JS_TEXT = 'verified';

/**
* @var string[]
*/
private const LOG_LEVELS = ['debug', 'info', 'error', 'warning'];

public bool $hasCpSettings = true;

Expand Down Expand Up @@ -82,6 +92,7 @@ function (Event $event): void {

$honeypotValue = null;
$timetrapValue = null;
$jsHoneypotValue = null;

if ($settings->honeypotFieldName !== null) {
$honeypotValue = $request->getBodyParam($settings->honeypotFieldName);
Expand All @@ -92,14 +103,26 @@ function (Event $event): void {
$timetrapValue = $request->getBodyParam($settings->timetrapFieldName);
}

if ($honeypotValue === null && $timetrapValue === null) {
if ($settings->jsHoneypotFieldName !== null) {
/** @var ?string $jsHoneypotValue */
$jsHoneypotValue = $request->getBodyParam($settings->jsHoneypotFieldName);
}

if ($honeypotValue === null && $timetrapValue === null && $jsHoneypotValue === null) {
// A bot simply has to remove the input fields altogether to bypass this check.
return;
}

$isSpamSubmission = false;
$spamReasons = [];

// Honeypot test
if (! empty($honeypotValue)) {
$isSpamSubmission = true;
$spamReasons[] = 'Honeypot value was set';
}

// Timetrap test
if ($timetrapValue !== null) {
$timetrapValue = $this->decodeTimestamp($timetrapValue);
if ($timetrapValue === false) {
Expand All @@ -118,9 +141,10 @@ function (Event $event): void {
}
}

if (! empty($honeypotValue)) {
// Js honeypot test
if ($jsHoneypotValue !== null && $jsHoneypotValue !== self::DEFAULT_JS_TEXT) {
$isSpamSubmission = true;
$spamReasons[] = 'Honeypot value was set';
$spamReasons[] = 'JavaScript honeypot value was not set';
}

if ($isSpamSubmission) {
Expand Down
26 changes: 23 additions & 3 deletions src/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,31 @@
*
* This should be unique so that it does not conflict with any of your form inputs.
*/
'honeypotFieldName' => 'my_password',
'honeypotFieldName' => 'set_my_password',

'timeTrapFieldName' => 'honeypot_timetrap',
/**
* If set, the honeypot will include a timetrap field.
*
* This should be unique so that it does not conflict with any of your form inputs.
*/
'timetrapFieldName' => 'honeypot_timetrap',

'timeTrapTimeout' => 2000,
/**
* The timeout to be used with the timetrap.
*/
'timetrapTimeout' => 2000,

/**
* If set, a hidden input field will be included and extra JavaScript to set the value after a timeout ({@see jsHoneypotTimeout})
*
* This should be unique so that it does not conflict with any of your form inputs.
*/
'jsHoneypotFieldName' => 'verified_submission',

/**
* The timeout to be used before the hidden input field is set on the client.
*/
'jsHoneypotTimeout' => 2000,

/**
* Set to a string value to enable a response on non-dev environment.
Expand Down
10 changes: 10 additions & 0 deletions src/models/Settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ class Settings extends Model
*/
public null|int|string $timetrapTimeout = 2000;

/**
* If set, a hidden input field will be included and extra JavaScript to set the value after a timeout ({@see jsHoneypotTimeout})
*/
public ?string $jsHoneypotFieldName = 'verified_submission';

/**
* The timeout to be used before the hidden input field is set on the client.
*/
public null|int|string $jsHoneypotTimeout = 2000;

/**
* Set to a string value to enable a response on non-dev environment.
*
Expand Down
44 changes: 34 additions & 10 deletions src/web/twig/Honeypot.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,30 +21,54 @@ static function (): false|string {
return false;
}

$idPrefix = Craft::$app->getSecurity()->generateRandomString(12);

$inputs = [];

if ($settings->honeypotFieldName !== null) {
$inputs[] = Html::textInput(
$settings->honeypotFieldName,
'',
[
'id' => sprintf('%s_%s', $idPrefix, $settings->honeypotFieldName),
'autocomplete' => 'off',
'tabindex' => '-1',
'style' => 'display:none; visibility:hidden; position:absolute; left:-9999px;',
],
);
}

if ($settings->timetrapFieldName !== null) {
$timestamp = (new \DateTimeImmutable())->format('Uv');
$inputs[] = Html::hiddenInput(
$settings->timetrapFieldName,
base64_encode(Craft::$app->getSecurity()->encryptByKey((string) $timestamp)),
base64_encode(Craft::$app->getSecurity()->encryptByKey($timestamp)),
[
'id' => $settings->timetrapFieldName,
]
'id' => sprintf('%s_%s', $idPrefix, $settings->timetrapFieldName),
],
);
}

if ($settings->honeypotFieldName !== null) {
$inputs[] = Html::textInput(
$settings->honeypotFieldName,
if ($settings->jsHoneypotFieldName !== null) {
$jsInputId = sprintf('%s_%s', $idPrefix, $settings->jsHoneypotFieldName);
$inputs[] = Html::hiddenInput(
$settings->jsHoneypotFieldName,
'',
[
'id' => $settings->honeypotFieldName,
'autocomplete' => 'off',
'tabindex' => '-1',
'style' => 'display:none; visibility:hidden; position:absolute; left:-9999px;',
'id' => $jsInputId,
],
);

$jsTimeout = $settings->jsHoneypotTimeout ?? Plugin::DEFAULT_JS_TIMEOUT;
$jsVerifiedText = Plugin::DEFAULT_JS_TEXT; // TODO add config for this

$inputs[] = <<<EOJS
<script type="text/javascript">
setTimeout(function () {
document.getElementById('{$jsInputId}').value = '{$jsVerifiedText}';
}, {$jsTimeout});
</script>
EOJS;
}

return implode('', $inputs);
Expand Down

0 comments on commit ea850c3

Please sign in to comment.