123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316 |
- <?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]);
- }
|