From c5754ba0b511fe159d9e397c5b72de5510d376e4 Mon Sep 17 00:00:00 2001 From: Bill Seremetis <bill@seremetis.net> Date: Wed, 28 May 2025 16:39:02 +0300 Subject: [PATCH] Honeypot on cached pages with JS --- config/install/honeypot.settings.yml | 1 + config/schema/honeypot.schema.yml | 3 ++ honeypot.install | 10 ++++++ honeypot.libraries.yml | 7 ++++ js/honeypot_timestamp.js | 54 ++++++++++++++++++++++++++++ src/Form/HoneypotSettingsForm.php | 39 +++++++++++++++++--- src/HoneypotService.php | 16 ++++++--- 7 files changed, 122 insertions(+), 8 deletions(-) create mode 100644 honeypot.libraries.yml create mode 100644 js/honeypot_timestamp.js diff --git a/config/install/honeypot.settings.yml b/config/install/honeypot.settings.yml index e70bdc2..a740c9a 100644 --- a/config/install/honeypot.settings.yml +++ b/config/install/honeypot.settings.yml @@ -8,6 +8,7 @@ protect_all_forms: false log: false element_name: 'url' time_limit: 5 +use_js_for_cached_pages: false expire: 300 form_settings: user_register_form: false diff --git a/config/schema/honeypot.schema.yml b/config/schema/honeypot.schema.yml index ec37c01..2519b58 100644 --- a/config/schema/honeypot.schema.yml +++ b/config/schema/honeypot.schema.yml @@ -19,6 +19,9 @@ honeypot.settings: time_limit: type: integer label: 'Honeypot time limit' + use_js_for_cached_pages: + type: boolean + label: 'Use Javascript protection' expire: type: integer label: 'Expiration' diff --git a/honeypot.install b/honeypot.install index dcf29f0..5ede7f4 100644 --- a/honeypot.install +++ b/honeypot.install @@ -76,3 +76,13 @@ function honeypot_uninstall() { function honeypot_update_last_removed() { return 8104; } + +/** + * Add default value for use_js_for_cached_pages. + */ +function honeypot_update_8105() { + $config_factory = \Drupal::configFactory(); + $config = $config_factory->getEditable('honeypot.settings'); + $config->set('use_js_for_cached_pages', FALSE); + $config->save(TRUE); +} diff --git a/honeypot.libraries.yml b/honeypot.libraries.yml new file mode 100644 index 0000000..f9c6001 --- /dev/null +++ b/honeypot.libraries.yml @@ -0,0 +1,7 @@ +timestamp: + js: + js/honeypot_timestamp.js: {} + dependencies: + - core/jquery + - core/once + - core/drupalSettings diff --git a/js/honeypot_timestamp.js b/js/honeypot_timestamp.js new file mode 100644 index 0000000..61b188d --- /dev/null +++ b/js/honeypot_timestamp.js @@ -0,0 +1,54 @@ +/** + * @file + */ +(function ($, Drupal, drupalSettings) { + + 'use strict'; + + Drupal.honeypot = {}; + + Drupal.honeypot.page_load_timestamp = new Date(); + + Drupal.behaviors.honeypot_timestamp = { + + + attach: function (context, settings) { + var obj = this; + var $honeypotTime = $('form.honeypot-timestamp-js').find('input[name="honeypot_time"]'); + + $(once('honeypot-timestamp', 'form.honeypot-timestamp-js')).bind('submit', function () { + $honeypotTime.attr('value', obj.getIntervalTimestamp()); + }); + }, + + getIntervalTimestamp: function () { + var now = new Date(); + var interval = Math.floor((now - Drupal.honeypot.page_load_timestamp) / 1000); + return 'js_token:' + drupalSettings.honeypot.identifier + '|' + interval; + } + }; + + if (Drupal.Ajax && Drupal.Ajax.prototype) { + if (typeof Drupal.Ajax.prototype.beforeSubmit != 'undefined') { + Drupal.Ajax.prototype.honeypotOriginalBeforeSubmit = Drupal.Ajax.prototype.beforeSubmit; + } + + Drupal.Ajax.prototype.beforeSubmit = function (form_values, element, options) { + + if (this.$form && this.$form.hasClass('honeypot-timestamp-js')) { + $.each(form_values, function(key, el) { + // Inject the right interval timestamp. + if (el.name == 'honeypot_time' && el.value == 'no_js_available') { + form_values[key].value = Drupal.behaviors.honeypot_timestamp.getIntervalTimestamp(); + } + }); + } + + if (typeof Drupal.Ajax.prototype.honeypotOriginalBeforeSubmit != 'undefined') { + // Call the original function in case someone else has overridden it. + return Drupal.Ajax.prototype.honeypotOriginalBeforeSubmit(form_values, element, options); + } + } + } + +}(jQuery, Drupal, drupalSettings)); diff --git a/src/Form/HoneypotSettingsForm.php b/src/Form/HoneypotSettingsForm.php index 485c256..0d3d656 100644 --- a/src/Form/HoneypotSettingsForm.php +++ b/src/Form/HoneypotSettingsForm.php @@ -3,6 +3,7 @@ namespace Drupal\honeypot\Form; use Drupal\Component\Utility\Html; +use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Config\TypedConfigManagerInterface; use Drupal\Core\Entity\EntityTypeBundleInfoInterface; @@ -40,7 +41,14 @@ class HoneypotSettingsForm extends ConfigFormBase { protected $entityTypeBundleInfo; /** - * Constructs a HoneypotSettings form. + * A cache backend interface. + * + * @var \Drupal\Core\Cache\CacheBackendInterface + */ + protected $cache; + + /** + * Constructs a settings controller. * * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * The factory for configuration objects. @@ -52,12 +60,15 @@ class HoneypotSettingsForm extends ConfigFormBase { * The entity type manager. * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info * The entity type bundle info service. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend + * The cache backend interface. */ - public function __construct(ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typedConfigManager, ModuleHandlerInterface $module_handler, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info) { + public function __construct(ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typedConfigManager, ModuleHandlerInterface $module_handler, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, CacheBackendInterface $cache_backend) { parent::__construct($config_factory, $typedConfigManager); $this->moduleHandler = $module_handler; $this->entityTypeManager = $entity_type_manager; $this->entityTypeBundleInfo = $entity_type_bundle_info; + $this->cache = $cache_backend; } /** @@ -69,7 +80,8 @@ class HoneypotSettingsForm extends ConfigFormBase { $container->get('config.typed'), $container->get('module_handler'), $container->get('entity_type.manager'), - $container->get('entity_type.bundle.info') + $container->get('entity_type.bundle.info'), + $container->get('cache.default') ); } @@ -130,7 +142,23 @@ class HoneypotSettingsForm extends ConfigFormBase { '#size' => 5, '#field_suffix' => $this->t('seconds'), ]; - $form['configuration']['time_limit']['#description'] .= '<br />' . $this->t('<strong>Page caching will be disabled if there is a form protected by time limit on the page.</strong>'); + + if (!$honeypot_config->get('use_js_for_cached_pages')) { + $form['configuration']['time_limit']['#description'] .= '<br />' . $this->t('<strong>Page caching will be disabled if there is a form protected by time limit on the page.</strong>'); + } + + $form['configuration']['use_js_for_cached_pages'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Use Javascript protection for cacheable pages. (experimental)'), + '#description' => $this->t('Uses Javascript to preserve Page caching'), + '#default_value' => $honeypot_config->get('use_js_for_cached_pages'), + '#states' => [ + 'invisible' => [ + 'input[name="time_limit"]' => ['value' => 0], + ], + ], + ]; + $form['configuration']['use_js_for_cached_pages']['#description'] .= '<br />' . t('<strong>Warning: Users who have Javascript disabled will need to confirm their form submission on the next page (if the Honeypot-enabled form is on a cacheable page).</strong>'); $form['configuration']['expire'] = [ '#type' => 'number', @@ -285,6 +313,9 @@ class HoneypotSettingsForm extends ConfigFormBase { // Save the honeypot forms from $form_state into a 'form_settings' array. $config->set('form_settings', $form_state->getValue('form_settings'))->save(); + // Clear the honeypot protected forms cache. + $this->cache->delete('honeypot_protected_forms'); + parent::submitForm($form, $form_state); } diff --git a/src/HoneypotService.php b/src/HoneypotService.php index 99e9e33..b2b952d 100644 --- a/src/HoneypotService.php +++ b/src/HoneypotService.php @@ -203,10 +203,18 @@ class HoneypotService implements HoneypotServiceInterface { // Disable page caching to make sure timestamp isn't cached. if ($this->account->id() == 0) { - // @todo D8 - Use DIC? - // @see https://www.drupal.org/node/1539454 - // Should this now set 'omit_vary_cookie' instead? - $this->killSwitch->trigger(); + if ($this->config->get('use_js_for_cached_pages')) { + // This value will be overwritten by JS in Drupal.behaviors.honeypot_timestamp. + $form['honeypot_time']['#default_value'] = 'no_js_available'; + $form['honeypot_time']['#attached']['library'][] = 'honeypot/timestamp'; + $form['#attached']['drupalSettings']['honeypot']['identifier'] = $identifier; + $form['#attributes']['class'][] = 'honeypot-timestamp-js'; + } + else { + // @todo Use dependency injection? See: http://drupal.org/node/1539454 + // Should this now set 'omit_vary_cookie' instead? + $this->killSwitch->trigger(); + } } } -- GitLab