Files
edlp_d8/sites/all/modules/contrib/security/honeypot/honeypot.module
T

317 lines
11 KiB
Plaintext

<?php
/**
* @file
* Honeypot module, for deterring spam bots from completing Drupal forms.
*/
use Drupal\Core\Form\FormStateInterface;
use Drupal\Component\Utility\Crypt;
/**
* Implements hook_cron().
*/
function honeypot_cron() {
// Delete {honeypot_user} entries older than the value of honeypot_expire.
$expire_limit = \Drupal::config('honeypot.settings')->get('expire');
\Drupal::database()->delete('honeypot_user')
->condition('timestamp', REQUEST_TIME - $expire_limit, '<')
->execute();
}
/**
* Implements hook_form_alter().
*
* Add Honeypot features to forms enabled in the Honeypot admin interface.
*/
function honeypot_form_alter(&$form, FormStateInterface $form_state, $form_id) {
// Don't use for maintenance mode forms (install, update, etc.).
if (defined('MAINTENANCE_MODE')) {
return;
}
// Add a tag to all forms, so that if they are cached and honeypot
// configuration is changed, the cached forms are invalidated and honeypot
// protection can be re-evaluated.
$form['#cache']['tags'][] = 'config:honeypot.settings';
// Get list of unprotected forms and setting for whether to protect all forms.
$unprotected_forms = \Drupal::config('honeypot.settings')->get('unprotected_forms');
$protect_all_forms = \Drupal::config('honeypot.settings')->get('protect_all_forms');
// If configured to protect all forms, add protection to every form.
if ($protect_all_forms && !in_array($form_id, $unprotected_forms)) {
// Don't protect system forms - only admins should have access, and system
// forms may be programmatically submitted by drush and other modules.
if (strpos($form_id, 'system_') === FALSE && strpos($form_id, 'search_') === FALSE && strpos($form_id, 'views_exposed_form_') === FALSE) {
honeypot_add_form_protection($form, $form_state, ['honeypot', 'time_restriction']);
}
}
// Otherwise add form protection to admin-configured forms.
elseif ($forms_to_protect = honeypot_get_protected_forms()) {
foreach ($forms_to_protect as $protect_form_id) {
// For most forms, do a straight check on the form ID.
if ($form_id == $protect_form_id) {
honeypot_add_form_protection($form, $form_state, ['honeypot', 'time_restriction']);
}
}
}
}
/**
* Build an array of all the protected forms on the site, by form_id.
*/
function honeypot_get_protected_forms() {
$forms = &drupal_static(__FUNCTION__);
// If the data isn't already in memory, get from cache or look it up fresh.
if (!isset($forms)) {
if ($cache = \Drupal::cache()->get('honeypot_protected_forms')) {
$forms = $cache->data;
}
else {
$form_settings = \Drupal::config('honeypot.settings')->get('form_settings');
if (!empty($form_settings)) {
// Add each form that's enabled to the $forms array.
foreach ($form_settings as $form_id => $enabled) {
if ($enabled) {
$forms[] = $form_id;
}
}
}
else {
$forms = [];
}
// Save the cached data.
\Drupal::cache()->set('honeypot_protected_forms', $forms);
}
}
return $forms;
}
/**
* Form builder function to add different types of protection to forms.
*
* @param array $options
* Array of options to be added to form. Currently accepts 'honeypot' and
* 'time_restriction'.
*/
function honeypot_add_form_protection(&$form, FormStateInterface $form_state, array $options = []) {
$account = \Drupal::currentUser();
// Allow other modules to alter the protections applied to this form.
\Drupal::moduleHandler()->alter('honeypot_form_protections', $options, $form);
// Don't add any protections if the user can bypass the Honeypot.
if ($account->hasPermission('bypass honeypot protection')) {
return;
}
// Build the honeypot element.
if (in_array('honeypot', $options)) {
// Get the element name (default is generic 'url').
$honeypot_element = \Drupal::config('honeypot.settings')->get('element_name');
// Build the honeypot element.
$honeypot_class = $honeypot_element . '-textfield';
$form[$honeypot_element] = [
'#theme_wrappers' => [
'container' => [
'#id' => NULL,
'#attributes' => [
'class' => [
$honeypot_class,
],
'style' => [
'display: none !important;',
],
],
],
],
'#type' => 'textfield',
'#title' => t('Leave this field blank'),
'#size' => 20,
'#weight' => 100,
'#attributes' => ['autocomplete' => 'off'],
'#element_validate' => ['_honeypot_honeypot_validate'],
];
}
// Set the time restriction for this form (if it's not disabled).
if (in_array('time_restriction', $options) && \Drupal::config('honeypot.settings')->get('time_limit') != 0) {
// Set the current time in a hidden value to be checked later.
$input = $form_state->getUserInput();
if (empty($input['honeypot_time'])) {
$identifier = Crypt::randomBytesBase64();
\Drupal::service('keyvalue.expirable')->get('honeypot_time_restriction')->set($identifier, time(), 3600*24);
}
else {
$identifier = $input['honeypot_time'];
}
$form['honeypot_time'] = [
'#type' => 'hidden',
'#title' => t('Timestamp'),
'#default_value' => $identifier,
'#element_validate' => ['_honeypot_time_restriction_validate'],
'#cache' => [
'max-age' => 0,
],
];
// Disable page caching to make sure timestamp isn't cached.
$account = \Drupal::currentUser();
if ($account->id() == 0) {
// TODO D8 - Use DIC? See: http://drupal.org/node/1539454
// Should this now set 'omit_vary_cookie' instead?
Drupal::service('page_cache_kill_switch')->trigger();
}
}
// Allow other modules to react to addition of form protection.
if (!empty($options)) {
\Drupal::moduleHandler()->invokeAll('honeypot_add_form_protection', [$options, $form]);
}
}
/**
* Validate honeypot field.
*/
function _honeypot_honeypot_validate($element, FormStateInterface $form_state) {
// Get the honeypot field value.
$honeypot_value = $element['#value'];
// Make sure it's empty.
if (!empty($honeypot_value)) {
_honeypot_log($form_state->getValue('form_id'), 'honeypot');
$form_state->setErrorByName('', t('There was a problem with your form submission. Please refresh the page and try again.'));
}
}
/**
* Validate honeypot's time restriction field.
*/
function _honeypot_time_restriction_validate($element, FormStateInterface $form_state) {
if ($form_state->isProgrammed()) {
// Don't do anything if the form was submitted programmatically.
return;
}
$triggering_element = $form_state->getTriggeringElement();
// Don't do anything if the triggering element is a preview button.
if ($triggering_element['#value'] == t('Preview')) {
return;
}
// Get the time value.
$identifier = $form_state->getValue('honeypot_time', FALSE);
$honeypot_time = \Drupal::service('keyvalue.expirable')->get('honeypot_time_restriction')->get($identifier, 0);
// Get the honeypot_time_limit.
$time_limit = honeypot_get_time_limit($form_state->getValues());
// Make sure current time - (time_limit + form time value) is greater than 0.
// If not, throw an error.
if (!$honeypot_time || REQUEST_TIME < ($honeypot_time + $time_limit)) {
_honeypot_log($form_state->getValue('form_id'), 'honeypot_time');
$time_limit = honeypot_get_time_limit();
\Drupal::service('keyvalue.expirable')->get('honeypot_time_restriction')->set($identifier, REQUEST_TIME, 3600*24);
$form_state->setErrorByName('', t('There was a problem with your form submission. Please wait @limit seconds and try again.', ['@limit' => $time_limit]));
}
}
/**
* Log blocked form submissions.
*
* @param string $form_id
* Form ID for the form on which submission was blocked.
* @param string $type
* String indicating the reason the submission was blocked. Allowed values:
* - honeypot: If honeypot field was filled in.
* - honeypot_time: If form was completed before the configured time limit.
*/
function _honeypot_log($form_id, $type) {
honeypot_log_failure($form_id, $type);
if (\Drupal::config('honeypot.settings')->get('log')) {
$variables = [
'%form' => $form_id,
'@cause' => ($type == 'honeypot') ? t('submission of a value in the honeypot field') : t('submission of the form in less than minimum required time'),
];
\Drupal::logger('honeypot')->notice(t('Blocked submission of %form due to @cause.', $variables));
}
}
/**
* Look up the time limit for the current user.
*
* @param array $form_values
* Array of form values (optional).
*/
function honeypot_get_time_limit(array $form_values = []) {
$account = \Drupal::currentUser();
$honeypot_time_limit = \Drupal::config('honeypot.settings')->get('time_limit');
// Only calculate time limit if honeypot_time_limit has a value > 0.
if ($honeypot_time_limit) {
$expire_time = \Drupal::config('honeypot.settings')->get('expire');
// Query the {honeypot_user} table to determine the number of failed
// submissions for the current user.
$uid = $account->id();
$query = \Drupal::database()->select('honeypot_user', 'hu')
->condition('uid', $uid)
->condition('timestamp', REQUEST_TIME - $expire_time, '>');
// For anonymous users, take the hostname into account.
if ($uid === 0) {
$hostname = \Drupal::request()->getClientIp();
$query->condition('hostname', $hostname);
}
$number = $query->countQuery()->execute()->fetchField();
// Don't add more than 30 days' worth of extra time.
$honeypot_time_limit = (int) min($honeypot_time_limit + exp($number) - 1, 2592000);
// TODO - Only accepts two args.
$additions = \Drupal::moduleHandler()->invokeAll('honeypot_time_limit', [
$honeypot_time_limit,
$form_values,
$number,
]);
if (count($additions)) {
$honeypot_time_limit += array_sum($additions);
}
}
return $honeypot_time_limit;
}
/**
* Log the failed submission with timestamp and hostname.
*
* @param string $form_id
* Form ID for the rejected form submission.
* @param string $type
* String indicating the reason the submission was blocked. Allowed values:
* - honeypot: If honeypot field was filled in.
* - honeypot_time: If form was completed before the configured time limit.
*/
function honeypot_log_failure($form_id, $type) {
$account = \Drupal::currentUser();
$uid = $account->id();
// Log failed submissions.
\Drupal::database()->insert('honeypot_user')
->fields([
'uid' => $uid,
'hostname' => Drupal::request()->getClientIp(),
'timestamp' => REQUEST_TIME,
])
->execute();
// Allow other modules to react to honeypot rejections.
// TODO - Only accepts two args.
\Drupal::moduleHandler()->invokeAll('honeypot_reject', [$form_id, $uid, $type]);
}