diff --git a/composer.json b/composer.json index bb659dd2c2e5540084dbd671e222f7e945966a6c..8209f87e1eacfcb09640bd208b3dc62ebbea0bb3 100644 --- a/composer.json +++ b/composer.json @@ -125,7 +125,7 @@ "drupal/focal_point": "1.5", "drupal/google_analytics": "^4.0", "drupal/google_tag": "1.4", - "drupal/honeypot": "2.0.1", + "drupal/honeypot": "2.1.0", "drupal/inline_entity_form": "1.0-rc9", "drupal/libraries": "3.0-beta1", "drupal/link_attributes": "1.11", @@ -285,7 +285,7 @@ "3060223": "https://www.drupal.org/files/issues/2019-10-17/%20entity_clone-corrupted-paragraph-cloning-3060223-5.patch" }, "drupal/honeypot": { - "2811189": "https://www.drupal.org/files/issues/2019-08-08/honeypot_field_weight_2811189-18.patch" + "2811189": "https://www.drupal.org/files/issues/2022-05-25/honeypot-field_weight-2811189-27_0.patch" }, "drupal/inline_entity_form": { "3208279": "https://www.drupal.org/files/issues/2021-05-08/inline_entity_form-n3208279-13.patch" diff --git a/composer.lock b/composer.lock index 687077f3cb32a4e794b2fbba52e08d6ba3054dbd..87418fa8ec7734b9591d253d63ccc55bdf54708c 100644 --- a/composer.lock +++ b/composer.lock @@ -4630,26 +4630,29 @@ }, { "name": "drupal/honeypot", - "version": "2.0.1", + "version": "2.1.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/honeypot.git", - "reference": "2.0.1" + "reference": "2.1.0" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/honeypot-2.0.1.zip", - "reference": "2.0.1", - "shasum": "c29d248c0fdcdf733a31b9214355acfa73716632" + "url": "https://ftp.drupal.org/files/projects/honeypot-2.1.0.zip", + "reference": "2.1.0", + "shasum": "7ddb2d0bfeaa65d55823d82bdf01c9c330a1e12f" }, "require": { - "drupal/core": "^8.0 || ^9.0" + "drupal/core": "^9.2 || ^10" + }, + "require-dev": { + "drupal/rules": "^3.0" }, "type": "drupal-module", "extra": { "drupal": { - "version": "2.0.1", - "datestamp": "1597855128", + "version": "2.1.0", + "datestamp": "1651894953", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 34095dbd1cf8e546ca2cabcb0f44eee31c47269d..fa42a73416296dc96a48a18ee42be0c3fafe8f3e 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -4785,34 +4785,37 @@ }, { "name": "drupal/honeypot", - "version": "2.0.1", - "version_normalized": "2.0.1.0", + "version": "2.1.0", + "version_normalized": "2.1.0.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/honeypot.git", - "reference": "2.0.1" + "reference": "2.1.0" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/honeypot-2.0.1.zip", - "reference": "2.0.1", - "shasum": "c29d248c0fdcdf733a31b9214355acfa73716632" + "url": "https://ftp.drupal.org/files/projects/honeypot-2.1.0.zip", + "reference": "2.1.0", + "shasum": "7ddb2d0bfeaa65d55823d82bdf01c9c330a1e12f" }, "require": { - "drupal/core": "^8.0 || ^9.0" + "drupal/core": "^9.2 || ^10" + }, + "require-dev": { + "drupal/rules": "^3.0" }, "type": "drupal-module", "extra": { "drupal": { - "version": "2.0.1", - "datestamp": "1597855128", + "version": "2.1.0", + "datestamp": "1651894953", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" } }, "patches_applied": { - "2811189": "https://www.drupal.org/files/issues/2019-08-08/honeypot_field_weight_2811189-18.patch" + "2811189": "https://www.drupal.org/files/issues/2022-05-25/honeypot-field_weight-2811189-27_0.patch" } }, "installation-source": "dist", @@ -4826,6 +4829,10 @@ "homepage": "https://www.drupal.org/user/213194", "email": "geerlingguy@mac.com" }, + { + "name": "TR", + "homepage": "https://www.drupal.org/user/202830" + }, { "name": "geerlingguy", "homepage": "https://www.drupal.org/user/389011" diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index 20602f88a335bf919d0a5985b035c8d572123802..e1570644f305aa7ef2751f25df9954be44d4c7f2 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -1058,12 +1058,12 @@ ), ), 'drupal/honeypot' => array( - 'pretty_version' => '2.0.1', - 'version' => '2.0.1.0', + 'pretty_version' => '2.1.0', + 'version' => '2.1.0.0', 'type' => 'drupal-module', 'install_path' => __DIR__ . '/../../web/modules/honeypot', 'aliases' => array(), - 'reference' => '2.0.1', + 'reference' => '2.1.0', 'dev_requirement' => false, ), 'drupal/image' => array( diff --git a/web/modules/honeypot/.travis.yml b/web/modules/honeypot/.travis.yml deleted file mode 100644 index f5d52a03543d46640f024878f7ed1d82a34d637a..0000000000000000000000000000000000000000 --- a/web/modules/honeypot/.travis.yml +++ /dev/null @@ -1,47 +0,0 @@ ---- -language: php -php: '7.2' -services: docker - -env: - DOCKER_COMPOSE_VERSION: 1.23.2 - -before_install: - - sudo service mysql stop - - # Upgrade docker-compose. - - sudo rm /usr/local/bin/docker-compose - - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose - - chmod +x docker-compose - - sudo mv docker-compose /usr/local/bin - -install: - # Build environment. - - docker-compose up -d - - # Wait for composer create-project to complete. - - sleep 300 - - # Structure the codebase and install necessary dependencies. - - docker-compose exec drupal bash -c 'apt-get update && apt-get install -y sudo' - - docker-compose exec drupal bash -c 'composer config platform --unset' - - docker-compose exec drupal bash -c 'composer require --prefer-source --no-interaction --dev drush/drush' - - docker-compose exec drupal bash -c 'composer update phpunit/phpunit symfony/phpunit-bridge phpspec/prophecy symfony/yaml --with-dependencies' - - docker-compose exec drupal ln -s /opt/honeypot/ /var/www/html/web/modules/honeypot - - # Install Drupal and Honeypot/Testing. - - docker-compose exec drupal bash -c 'sudo -u www-data vendor/bin/drush site:install standard --site-name="Honeypot Test" --account-pass admin -y' - - docker-compose exec drupal bash -c 'vendor/bin/drush en -y honeypot simpletest' - -before_script: - # Adjust permissions on the simpletest directories. - - docker exec honeypot mkdir -p /var/www/html/web/sites/simpletest - - docker exec honeypot chown -R www-data:www-data /var/www/html/web/sites/simpletest - -script: - # Run module tests. - - docker-compose exec drupal bash -c 'sudo -u www-data php web/core/scripts/run-tests.sh --module honeypot --url http://localhost/' - -after_failure: - # Re-run tests with verbose output for debugging. - - docker-compose exec drupal bash -c 'sudo -u www-data php web/core/scripts/run-tests.sh --verbose --module honeypot --url http://localhost/' diff --git a/web/modules/honeypot/PATCHES.txt b/web/modules/honeypot/PATCHES.txt index dfab2f981967ed6184cbe6f186ad63b3bc8d5d5b..44cc610c7bad2aa3fec6df0605b4153b2d79e03f 100644 --- a/web/modules/honeypot/PATCHES.txt +++ b/web/modules/honeypot/PATCHES.txt @@ -2,6 +2,6 @@ This file was automatically generated by Composer Patches (https://github.com/cw Patches applied to this directory: 2811189 -Source: https://www.drupal.org/files/issues/2019-08-08/honeypot_field_weight_2811189-18.patch +Source: https://www.drupal.org/files/issues/2022-05-25/honeypot-field_weight-2811189-27_0.patch diff --git a/web/modules/honeypot/README.md b/web/modules/honeypot/README.md index c096502ad351f2e5e2a758c8cf07942c50710fe6..1766454b40034c38a270265e71f5c6e837cc26ed 100644 --- a/web/modules/honeypot/README.md +++ b/web/modules/honeypot/README.md @@ -6,7 +6,7 @@ ## Installation -To install this module, `composer require` it, or place it in your modules +To install this module, `composer require` it, or place it in your modules folder and enable it on the modules page. @@ -14,7 +14,7 @@ folder and enable it on the modules page. All settings for this module are on the Honeypot configuration page, under the Configuration section, in the Content authoring settings. You can visit the -configuration page directly at admin/config/content/honeypot. +configuration page directly at /admin/config/content/honeypot. Note that, when testing Honeypot on your website, make sure you're not logged in as an administrative user or user 1; Honeypot allows administrative users to @@ -28,7 +28,7 @@ If you want to add honeypot to your own forms, or to any form through your own module's hook_form_alter's, you can simply place the following function call inside your form builder function (or inside a hook_form_alter): - honeypot_add_form_protection( + \Drupal::service('honeypot')->addFormProtection( $form, $form_state, ['honeypot', 'time_restriction'] @@ -40,10 +40,12 @@ restriction on the form by including or not including the option in the array. ## Testing -Honeypot includes a `docker-compose.yml` file that can be used for testing purposes. To build a Drupal 8 environment for local testing, do the following: +Honeypot includes a `docker-compose.yml` file that can be used for testing +purposes. To build a Drupal 8 environment for local testing, do the following: - 1. Make sure you have Docker for Mac (or for whatever OS you're using) installed. - 1. Run the following commands in this directory to start the environment and install Drush: + 1. Make sure you have Docker installed. + 1. Run the following commands in this directory to start the environment and + install Drush: ``` docker-compose up -d @@ -63,7 +65,8 @@ Honeypot includes a `docker-compose.yml` file that can be used for testing purpo docker-compose exec drupal bash -c 'vendor/bin/drush site:install standard --site-name="Honeypot Test" --account-pass admin -y && chown -R www-data:www-data web/sites/default/files' ``` - 1. Log into `http://localhost/` with `admin`/`admin` and enable Honeypot (and the Testing module, if desired). + 1. Log into `http://localhost/` with `admin`/`admin` and enable Honeypot (and + the Testing module, if desired). ## Credit diff --git a/web/modules/honeypot/composer.json b/web/modules/honeypot/composer.json index 7ee3c1b8f51eb65914aeee57df04bc92c2fc6384..868aea98231e3f28e8ea7680dd4e0b968139630c 100644 --- a/web/modules/honeypot/composer.json +++ b/web/modules/honeypot/composer.json @@ -1,22 +1,25 @@ { - "name": "drupal/honeypot", - "description": "Mitigates spam form submissions using the honeypot method.", - "type": "drupal-module", - "license": "GPL-2.0-or-later", - "keywords": ["spam", "php", "form", "honeypot", "honeytrap", "deterrent"], - "homepage": "https://www.drupal.org/project/honeypot", - "minimum-stability": "dev", - "authors": [ - { - "name": "Jeff Geerling", - "email": "geerlingguy@mac.com" - } - ], - "support": { - "issues": "https://www.drupal.org/project/issues/honeypot", - "source": "https://git.drupalcode.org/project/honeypot" - }, - "require": { - "drupal/core": "^8.0 || ^9.0" + "name": "drupal/honeypot", + "description": "Mitigates spam form submissions using the honeypot method.", + "type": "drupal-module", + "license": "GPL-2.0-or-later", + "keywords": ["spam", "php", "form", "honeypot", "honeytrap", "deterrent"], + "homepage": "https://www.drupal.org/project/honeypot", + "minimum-stability": "dev", + "authors": [ + { + "name": "Jeff Geerling", + "email": "geerlingguy@mac.com" + } + ], + "support": { + "issues": "https://www.drupal.org/project/issues/honeypot", + "source": "https://git.drupalcode.org/project/honeypot" + }, + "require": { + "drupal/core": "^9.2 || ^10" + }, + "require-dev": { + "drupal/rules": "^3.0" } } diff --git a/web/modules/honeypot/config/optional/tour.tour.honeypot.yml b/web/modules/honeypot/config/optional/tour.tour.honeypot.yml index 1de333a555490f1830c8910cc17f5d431bc8bdb7..af082f4238e7abb85df2705cec8574861e55d905 100644 --- a/web/modules/honeypot/config/optional/tour.tour.honeypot.yml +++ b/web/modules/honeypot/config/optional/tour.tour.honeypot.yml @@ -11,49 +11,52 @@ tips: label: Honeypot weight: -10 body: "Congratulations on installing Honeypot on your site! With just a few clicks, you can have your site well-protected against automated spam bots.\r\n\r\nClick Next to be guided through this configuration page." - location: top + location: top-start protect-all-forms: id: protect-all-forms plugin: text label: 'Protect all forms' weight: -9 - attributes: - data-id: edit-protect-all-forms + selector: '#edit-protect-all-forms' body: "Protecting all the forms is the easiest way to quickly cut down on spam on your site, but doing this disables Drupal's caching for every page where a form is displayed.\r\n\r\nNote: If you have the honeypot time limit enabled, this option may cause issues with Drupal Commerce product forms or similarly-sparse forms that are able to be completed in a very short time." - location: bottom + location: bottom-start log-blocked-form-submissions: id: log-blocked-form-submissions plugin: text label: 'Log blocked form submissions' weight: -8 - attributes: - data-id: edit-log + selector: '#edit-log' body: 'Check this box to log every form submission using watchdog. If you have Database Logging enabled, you can view these log entries in the Recent log messages page under Reports.' - location: bottom + location: bottom-start honeypot-element-name: id: honeypot-element-name plugin: text label: 'Honeypot Element Name' weight: -7 - attributes: - data-id: edit-element-name + selector: '#edit-element-name' body: 'Spam bots typically fill out any field they believe will help get links back to their site, so tempting them with a field named something like ''url'', ''homepage'', or ''link'' makes it hard for them to resist filling in the field—and easy to catch them in the trap and reject their submissions!' - location: top + location: top-start honeypot-time-limit: id: honeypot-time-limit plugin: text label: 'Honeypot Time Limit' weight: -6 - attributes: - data-id: edit-time-limit + selector: '#edit-time-limit' body: 'If you enter a positive value, Honeypot will require that all protected forms take at least that many seconds long to fill out. Most forms take at least 5-10 seconds to complete (if you''re a human), so setting this to a value < 5 will help protect against spam bots. Set to 0 to disable.' - location: top + location: top-start + honeypot-expire: + id: honeypot-expire + plugin: text + label: 'Honeypot Expire' + weight: -5 + selector: '#edit-expire' + body: 'If you enter a positive value, Honeypot will require that all protected forms take at least that many seconds long to fill out. Most forms take at least 5-10 seconds to complete (if you''re a human), so setting this to a value < 5 will help protect against spam bots. Set to 0 to disable.' + location: top-start honeypot-form-specific-settings: id: honeypot-form-specific-settings plugin: text label: 'Honeypot form-specific settings' - weight: -5 - attributes: - data-id: edit-form-settings + weight: -4 + selector: '#edit-form-settings' body: 'If you would like to choose particular forms to be protected by Honeypot, check the forms you wish to protect in this section. Most common types of forms are available for protection.' - location: top + location: top-start diff --git a/web/modules/honeypot/docker-compose.yml b/web/modules/honeypot/docker-compose.yml deleted file mode 100644 index 3179e0438bb2d4bf30f1054ca959ee3b3b0c41f9..0000000000000000000000000000000000000000 --- a/web/modules/honeypot/docker-compose.yml +++ /dev/null @@ -1,37 +0,0 @@ -version: "3" - -services: - drupal: - image: geerlingguy/drupal - container_name: honeypot - environment: - DRUPAL_DATABASE_HOST: 'mysql' - DRUPAL_DATABASE_PORT: '3306' - DRUPAL_DATABASE_NAME: 'drupal' - DRUPAL_DATABASE_USERNAME: 'drupal' - DRUPAL_DATABASE_PASSWORD: 'drupal' - DRUPAL_HASH_SALT: 'fe918c992fb1bcfa01f32303c8b21f3d0a0' - DRUPAL_DOWNLOAD_IF_NOT_PRESENT: 'true' - DRUPAL_DOWNLOAD_METHOD: 'composer' - DRUPAL_PROJECT_ROOT: /var/www/html - APACHE_DOCUMENT_ROOT: /var/www/html/web - COMPOSER_MEMORY_LIMIT: '-1' - ports: - - "80:80" - restart: always - volumes: - - ./:/opt/honeypot/:rw,delegated - - mysql: - image: mysql:5.7 - container_name: drupal-mysql - command: ['--max_allowed_packet=32505856'] - environment: - MYSQL_RANDOM_ROOT_PASSWORD: 'yes' - MYSQL_DATABASE: drupal - MYSQL_USER: drupal - MYSQL_PASSWORD: drupal - ports: - - "3306:3306" - volumes: - - /var/lib/mysql diff --git a/web/modules/honeypot/drupalci.yml b/web/modules/honeypot/drupalci.yml deleted file mode 100644 index 9adfb33938e5b3d45c3f6c0dea51a36b19e00e84..0000000000000000000000000000000000000000 --- a/web/modules/honeypot/drupalci.yml +++ /dev/null @@ -1,25 +0,0 @@ -# Learn to make one for your own drupal.org project: -# https://www.drupal.org/drupalorg/docs/drupal-ci/customizing-drupalci-testing -build: - assessment: - validate_codebase: - phplint: - container_composer: - phpcs: - # phpcs will use core's specified version of Coder. - sniff-all-files: true - halt-on-fail: false - testing: - # run_tests task is executed several times in order of performance speeds. - # halt-on-fail can be set on the run_tests tasks in order to fail fast. - # suppress-deprecations is false in order to be alerted to usages of - # deprecated code. - run_tests.standard: - types: 'Simpletest,PHPUnit-Unit,PHPUnit-Kernel,PHPUnit-Functional' - testgroups: '--all' - suppress-deprecations: false - run_tests.js: - types: 'PHPUnit-FunctionalJavascript' - testgroups: '--all' - suppress-deprecations: false - nightwatchjs: { } diff --git a/web/modules/honeypot/honeypot.api.php b/web/modules/honeypot/honeypot.api.php index efe433224b6aaab10ea625a6da057f4ebe2e1fff..9d90900cacebdb0f1193b90aa32e5a3b5fa28509 100644 --- a/web/modules/honeypot/honeypot.api.php +++ b/web/modules/honeypot/honeypot.api.php @@ -58,8 +58,8 @@ function hook_honeypot_add_form_protection(array $options, array $form) { * 0 for anonymous users, otherwise the user ID of the user. * @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. + * - honeypot: If honeypot field was filled in. + * - honeypot_time: If form was completed before the configured time limit. */ function hook_honeypot_reject($form_id, $uid, $type) { if ($form_id == 'mymodule_form') { diff --git a/web/modules/honeypot/honeypot.info.yml b/web/modules/honeypot/honeypot.info.yml index dcd9a9ae1b27870a9da86a5fe8201f33668d669c..287bff6fffcf8a703b411eedf74bc15d4cc424be 100644 --- a/web/modules/honeypot/honeypot.info.yml +++ b/web/modules/honeypot/honeypot.info.yml @@ -1,13 +1,14 @@ name: Honeypot type: module description: 'Mitigates spam form submissions using the honeypot method.' -package: "Spam control" -core: 8.x -core_version_requirement: ^8 || ^9 +package: 'Spam control' +core_version_requirement: ^9.2 || ^10 +test_dependencies: + - rules:rules + configure: honeypot.config -hidden: false -# Information added by Drupal.org packaging script on 2020-08-19 -version: '2.0.1' +# Information added by Drupal.org packaging script on 2022-05-07 +version: '2.1.0' project: 'honeypot' -datestamp: 1597855130 +datestamp: 1651894956 diff --git a/web/modules/honeypot/honeypot.install b/web/modules/honeypot/honeypot.install index 33ed67abcf0e6985f81266184e681b50dd8c47b1..edc958edc3bdf11951e6243b7da2235d6262914f 100644 --- a/web/modules/honeypot/honeypot.install +++ b/web/modules/honeypot/honeypot.install @@ -2,7 +2,7 @@ /** * @file - * Contains install and update functions for Honeypot. + * Install, update and uninstall functions for the Honeypot module. */ use Drupal\Core\Url; @@ -46,11 +46,9 @@ function honeypot_schema() { */ function honeypot_install() { if (PHP_SAPI !== 'cli') { - $config_url = Url::fromUri('base://admin/config/content/honeypot'); - \Drupal::messenger()->addMessage(t( - 'Honeypot installed successfully. Please <a href=":url">configure Honeypot</a> to protect your forms from spam bots.', - [':url' => $config_url->toString()] - )); + \Drupal::messenger()->addMessage(t('Honeypot installed successfully. Please <a href=":url">configure Honeypot</a> to protect your forms from spam bots.', [ + ':url' => Url::fromRoute('honeypot.config')->toString(), + ])); } } diff --git a/web/modules/honeypot/honeypot.module b/web/modules/honeypot/honeypot.module index b54fd28ca58674c3b47c4fccfa44849fbf53413c..b1fcdf2a00d9e8c20152ed9e69bb15c2a1622990 100644 --- a/web/modules/honeypot/honeypot.module +++ b/web/modules/honeypot/honeypot.module @@ -6,8 +6,9 @@ */ use Drupal\Core\Form\FormStateInterface; -use Drupal\Component\Utility\Crypt; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\Url; +use Drupal\honeypot\Event\HoneypotRejectEvent; /** * Implements hook_help(). @@ -17,13 +18,13 @@ function honeypot_help($route_name, RouteMatchInterface $route_match) { case 'help.page.honeypot': $output = ''; $output .= '<h3>' . t('About') . '</h3>'; - $output .= '<p>' . t('The Honeypot module uses both the honeypot and timestamp methods of deterring spam bots from completing forms on your Drupal site. These methods are effective against many spam bots, and are not as intrusive as CAPTCHAs or other methods which punish the user. For more information, see the <a href=":url">online documentation for the Honeypot module</a>.', [':url' => 'https://www.drupal.org/docs/8/modules/honeypot']) . '</p>'; + $output .= '<p>' . t('The Honeypot module uses both the honeypot and timestamp methods of deterring spam bots from completing forms on your Drupal site. These methods are effective against many spam bots, and are not as intrusive as CAPTCHAs or other methods which punish the user. For more information, see the <a href=":url">online documentation for the Honeypot module</a>.', [':url' => 'https://www.drupal.org/docs/contributed-modules/honeypot']) . '</p>'; $output .= '<h3>' . t('Uses') . '</h3>'; $output .= '<dl>'; $output .= '<dt>' . t('Configuring Honeypot') . '</dt>'; - $output .= '<dd>' . t('All settings for this module are on the Honeypot configuration page, under the Configuration section, in the Content authoring settings. You can visit the configuration page directly from the Honeypot configuration link below. The configuration settings are described in the <a href=":url">online documentation for the Honeypot module</a>.', [':url' => 'https://www.drupal.org/docs/8/modules/honeypot/using-honeypot']) . '</dd>'; + $output .= '<dd>' . t('All settings for this module are on the Honeypot configuration page, under the Configuration section, in the Content authoring settings. You can visit the configuration page directly from the Honeypot configuration link below. The configuration settings are described in the <a href=":url">online documentation for the Honeypot module</a>.', [':url' => 'https://www.drupal.org/docs/contributed-modules/honeypot/using-honeypot']) . '</dd>'; $output .= '<dt>' . t('Setting up Honeypot in your own forms') . '</dt>'; - $output .= '<dd>' . t("Honeypot protection can be bypassed for certain user roles. For instance, site administrators, who just might be able to fill out a form in less than 5 seconds. And, Honeypot protection can be enabled only for certain forms. Or, it can protect all forms on the site. Finally, honeypot protection can be used in any of your own forms by simply including a little code snippet included on the module's project page.") . '</dd>'; + $output .= '<dd>' . t("Honeypot protection can be bypassed for certain user roles. For instance, site administrators, who just might be able to fill out a form in less than 5 seconds. And, Honeypot protection can be enabled only for certain forms. Or, it can protect all forms on the site. Finally, honeypot protection can be used in any of your own forms by simply including a little code snippet included on the module's project page.") . '</dd>'; $output .= '</dl>'; return $output; } @@ -33,17 +34,41 @@ function honeypot_help($route_name, RouteMatchInterface $route_match) { * Implements hook_cron(). */ function honeypot_cron() { - // Delete {honeypot_user} entries older than the value of honeypot_expire. + // Delete {honeypot_user} entries older than the value of 'expire'. $expire_limit = \Drupal::config('honeypot.settings')->get('expire'); \Drupal::database()->delete('honeypot_user') ->condition('timestamp', \Drupal::time()->getRequestTime() - $expire_limit, '<') ->execute(); } +/** + * Implements hook_form_FORM_ID_alter(). + */ +function honeypot_form_system_performance_settings_alter(&$form, FormStateInterface $form_state, $form_id) { + // If time-based protection is effectively disabled, no need for a warning. + if (\Drupal::config('honeypot.settings')->get('time_limit') === 0) { + return; + } + + // Add a warning about caching on the Performance settings page. + $description = ''; + if (!empty($form['caching']['page_cache_maximum_age']['#description'])) { + // If there's existing description on 'caching' field, append a break to it + // so that our verbiage is on its own line. + $description .= $form['caching']['page_cache_maximum_age']['#description'] . '<br />'; + } + + $description .= t('<em>Page caching may be disabled on any pages where a form is present due to the <a href=":url">Honeypot module\'s configuration</a>.</em>', [ + ':url' => Url::fromRoute('honeypot.config')->toString(), + ]); + + $form['caching']['page_cache_maximum_age']['#description'] = $description; +} + /** * Implements hook_form_alter(). * - * Add Honeypot features to forms enabled in the Honeypot admin interface. + * Adds 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.). @@ -65,275 +90,103 @@ function honeypot_form_alter(&$form, FormStateInterface $form_state, $form_id) { // Don't protect system forms - only admins should have access, and system // forms may be programmatically submitted by drush and other modules. if (preg_match('/[^a-zA-Z]system_/', $form_id) === 0 && preg_match('/[^a-zA-Z]search_/', $form_id) === 0 && preg_match('/[^a-zA-Z]views_exposed_form_/', $form_id) === 0) { - honeypot_add_form_protection($form, $form_state, ['honeypot', 'time_restriction']); + \Drupal::service('honeypot')->addFormProtection($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']); - } - } + // Otherwise add form protection only to the admin-configured forms. + elseif (in_array($form_id, \Drupal::service('honeypot')->getProtectedForms())) { + // The $form_id of the form we're currently altering is found + // in the list of protected forms. + \Drupal::service('honeypot')->addFormProtection($form, $form_state, ['honeypot', 'time_restriction']); } } /** - * Build an array of all the protected forms on the site, by form_id. + * Builds an array of all the protected forms on the site. + * + * @return array + * An array whose values are the form_ids of all the protected forms + * on the site. + * + * @deprecated in honeypot:2.1.0 and is removed from honeypot:3.0.0. Use the + * 'honeypot' service instead. For example, \Drupal::service('honeypot') + * ->getProtectedForms(). + * + * @see https://www.drupal.org/node/2949447 */ 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; + @trigger_error("honeypot_get_protected_forms() is deprecated in honeypot:2.1.0 and is removed from honeypot:3.0.0. Use the 'honeypot' service instead. For example, \Drupal::service('honeypot')->getProtectedForms(). See https://www.drupal.org/node/2949447", E_USER_DEPRECATED); + return \Drupal::service('honeypot')->getProtectedForms(); } /** * 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' => [ - 0 => 'form_element', - 'container' => [ - '#id' => NULL, - '#attributes' => [ - 'class' => [ - $honeypot_class, - ], - 'style' => [ - 'display: none !important;', - ], - ], - ], - ], - '#type' => 'textfield', - '#title' => t('Leave this field blank'), - '#size' => 20, - '#weight' => -1, - '#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')->setWithExpire($identifier, time(), 3600 * 24); - } - else { - $identifier = $input['honeypot_time']; - } - $form['honeypot_time'] = [ - '#type' => 'hidden', - '#title' => t('Timestamp'), - '#weight' => -1, - '#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_value == '0') { - _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 || \Drupal::time()->getRequestTime() < ($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')->setWithExpire($identifier, \Drupal::time()->getRequestTime(), 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. + * (optional) Array of options to be added to form. Currently accepts + * 'honeypot' and 'time_restriction'. * - * @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. + * @deprecated in honeypot:2.1.0 and is removed from honeypot:3.0.0. Use the + * 'honeypot' service instead. For example, \Drupal::service('honeypot') + * ->addFormProtection($form, $form_state, $options). + * + * @see https://www.drupal.org/node/2949447 */ -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)); - } +function honeypot_add_form_protection(&$form, FormStateInterface $form_state, array $options = []) { + @trigger_error("honeypot_add_form_protection() is deprecated in honeypot:2.1.0 and is removed from honeypot:3.0.0. Use the 'honeypot' service instead. For example, \Drupal::service('honeypot')->addFormProtection(\$form, \$form_state, \$options). See https://www.drupal.org/node/2949447", E_USER_DEPRECATED); + \Drupal::service('honeypot')->addFormProtection($form, $form_state, $options); } /** - * Look up the time limit for the current user. + * Looks up the time limit for the current user. * * @param array $form_values - * Array of form values (optional). + * (optional) Array of form values. + * + * @return int + * The time limit in seconds. + * + * @deprecated in honeypot:2.1.0 and is removed from honeypot:3.0.0. Use the + * 'honeypot' service instead. For example, \Drupal::service('honeypot') + * ->getTimeLimit($form_values). + * + * @see https://www.drupal.org/node/2949447 */ 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', \Drupal::time()->getRequestTime() - $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, $expire_time); - // 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; + @trigger_error("honeypot_get_time_limit() is deprecated in honeypot:2.1.0 and is removed from honeypot:3.0.0. Use the 'honeypot' service instead. For example, \Drupal::service('honeypot')->getTimeLimit(\$form_values). See https://www.drupal.org/node/2949447", E_USER_DEPRECATED); + return \Drupal::service('honeypot')->getTimeLimit($form_values); } /** - * Log the failed submission with timestamp and hostname. + * Logs 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. + * - honeypot: If honeypot field was filled in. + * - honeypot_time: If form was completed before the configured time limit. + * + * @deprecated in honeypot:2.1.0 and is removed from honeypot:3.0.0. Use the + * 'honeypot' service instead. For example, \Drupal::service('honeypot') + * ->logFailure($form_id, $type). + * + * @see https://www.drupal.org/node/2949447 */ 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' => \Drupal::time()->getRequestTime(), - ]) - ->execute(); + @trigger_error("honeypot_log_failure() is deprecated in honeypot:2.1.0 and is removed from honeypot:3.0.0. Use the 'honeypot' service instead. For example, \Drupal::service('honeypot')->logFailure(\$form_id, \$type). See https://www.drupal.org/node/2949447", E_USER_DEPRECATED); + \Drupal::service('honeypot')->logFailure($form_id, $type); +} - // Allow other modules to react to honeypot rejections. - // TODO - Only accepts two args. - \Drupal::moduleHandler()->invokeAll('honeypot_reject', [$form_id, $uid, $type]); +/** + * Implements hook_honeypot_reject(). + * + * Generates an event when a form submission is rejected. + * + * @todo Only accepts two args - see above. + */ +function honeypot_honeypot_reject($form_id, $uid, $type) { + $event = new HoneypotRejectEvent($form_id, $uid, $type); + $event_dispatcher = \Drupal::service('event_dispatcher'); + $event_dispatcher->dispatch($event, $event::EVENT_NAME); } diff --git a/web/modules/honeypot/honeypot.routing.yml b/web/modules/honeypot/honeypot.routing.yml index 06c4963b1d8075c71566bb2cd041b54720ec93e4..62a294e3da22a09f172d5c38271b3bbd70d0778e 100644 --- a/web/modules/honeypot/honeypot.routing.yml +++ b/web/modules/honeypot/honeypot.routing.yml @@ -1,7 +1,7 @@ honeypot.config: path: '/admin/config/content/honeypot' defaults: - _form: '\Drupal\honeypot\Controller\HoneypotSettingsController' + _form: '\Drupal\honeypot\Form\HoneypotSettingsForm' _title: 'Honeypot configuration' requirements: _permission: 'administer honeypot' diff --git a/web/modules/honeypot/honeypot.rules.events.yml b/web/modules/honeypot/honeypot.rules.events.yml new file mode 100644 index 0000000000000000000000000000000000000000..a1df634170661ef9daaaa8c68e0609ef0a249389 --- /dev/null +++ b/web/modules/honeypot/honeypot.rules.events.yml @@ -0,0 +1,13 @@ +honeypot.form_submission_rejected: + label: 'After rejecting a form submission' + category: 'Honeypot' + context_definitions: + form_id: + type: 'string' + label: 'Rejected form ID' + uid: + type: 'integer' + label: 'Rejected user ID' + type: + type: 'string' + label: 'Reason for rejection' diff --git a/web/modules/honeypot/honeypot.services.yml b/web/modules/honeypot/honeypot.services.yml new file mode 100644 index 0000000000000000000000000000000000000000..331a8c2bdb08362945bd1c4e31056fd96f359562 --- /dev/null +++ b/web/modules/honeypot/honeypot.services.yml @@ -0,0 +1,4 @@ +services: + honeypot: + class: Drupal\honeypot\HoneypotService + arguments: ['@current_user', '@module_handler', '@config.factory', '@keyvalue.expirable', '@page_cache_kill_switch', '@database', '@logger.factory', '@datetime.time', '@string_translation', '@cache.default', '@request_stack'] diff --git a/web/modules/honeypot/src/Event/HoneypotRejectEvent.php b/web/modules/honeypot/src/Event/HoneypotRejectEvent.php new file mode 100644 index 0000000000000000000000000000000000000000..0ad0c74ee9428a659005245389febe0e57c37b71 --- /dev/null +++ b/web/modules/honeypot/src/Event/HoneypotRejectEvent.php @@ -0,0 +1,59 @@ +<?php + +namespace Drupal\honeypot\Event; + +use Drupal\Component\EventDispatcher\Event; + +/** + * Event that is fired when Honeypot rejects a form submission. + * + * @see hook_honeypot_reject() + */ +class HoneypotRejectEvent extends Event { + + const EVENT_NAME = 'honeypot.form_submission_rejected'; + + /** + * Form ID of the form the user was disallowed from submitting. + * + * @var string + */ + public $form_id; + + /** + * The user account ID. + * + * @var int + */ + public $uid; + + /** + * 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. + * + * @var string + */ + public $type; + + /** + * Constructs the object. + * + * @param string $form_id + * Form ID of the form the user was disallowed from submitting. + * @param int $uid + * The account of the user after unblocking. + * @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. + */ + public function __construct($form_id, int $uid, $type) { + $this->form_id = $form_id; + $this->uid = $uid; + $this->type = $type; + } + +} diff --git a/web/modules/honeypot/src/Controller/HoneypotSettingsController.php b/web/modules/honeypot/src/Form/HoneypotSettingsForm.php similarity index 81% rename from web/modules/honeypot/src/Controller/HoneypotSettingsController.php rename to web/modules/honeypot/src/Form/HoneypotSettingsForm.php index 18794d5a2acd857c5d935aa01bddbea6e8e9866d..040f12713c1bcbe653e53b22c5071f7ddb44d965 100644 --- a/web/modules/honeypot/src/Controller/HoneypotSettingsController.php +++ b/web/modules/honeypot/src/Form/HoneypotSettingsForm.php @@ -1,24 +1,21 @@ <?php -namespace Drupal\honeypot\Controller; +namespace Drupal\honeypot\Form; use Drupal\Component\Utility\Html; use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Form\FormStateInterface; -use Drupal\node\Entity\NodeType; -use Drupal\comment\Entity\CommentType; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\EntityTypeBundleInfoInterface; -use Drupal\Core\Cache\CacheBackendInterface; -use Drupal\Core\Messenger\MessengerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Returns responses for Honeypot module routes. */ -class HoneypotSettingsController extends ConfigFormBase { +class HoneypotSettingsForm extends ConfigFormBase { /** * The module handler service. @@ -48,13 +45,6 @@ class HoneypotSettingsController extends ConfigFormBase { */ protected $cache; - /** - * The Messenger service. - * - * @var \Drupal\Core\Messenger\MessengerInterface - */ - protected $messenger; - /** * Constructs a settings controller. * @@ -68,16 +58,13 @@ class HoneypotSettingsController extends ConfigFormBase { * The entity type bundle info service. * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend * The cache backend interface. - * @param \Drupal\Core\Messenger\MessengerInterface $messenger - * The messenger service. */ - public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, CacheBackendInterface $cache_backend, MessengerInterface $messenger) { + public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, CacheBackendInterface $cache_backend) { parent::__construct($config_factory); $this->moduleHandler = $module_handler; $this->entityTypeManager = $entity_type_manager; $this->entityTypeBundleInfo = $entity_type_bundle_info; $this->cache = $cache_backend; - $this->messenger = $messenger; } /** @@ -89,24 +76,15 @@ public static function create(ContainerInterface $container) { $container->get('module_handler'), $container->get('entity_type.manager'), $container->get('entity_type.bundle.info'), - $container->get('cache.default'), - $container->get('messenger') + $container->get('cache.default') ); } /** - * Get a value from the retrieved form settings array. + * {@inheritdoc} */ - public function getFormSettingsValue($form_settings, $form_id) { - // If there are settings in the array and the form ID already has a setting, - // return the saved setting for the form ID. - if (!empty($form_settings) && isset($form_settings[$form_id])) { - return $form_settings[$form_id]; - } - // Default to false. - else { - return 0; - } + public function getFormId() { + return 'honeypot_settings_form'; } /** @@ -116,18 +94,12 @@ protected function getEditableConfigNames() { return ['honeypot.settings']; } - /** - * {@inheritdoc} - */ - public function getFormId() { - return 'honeypot_settings_form'; - } - /** * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state) { // Honeypot Configuration. + $honeypot_config = $this->config('honeypot.settings'); $form['configuration'] = [ '#type' => 'fieldset', '#title' => $this->t('Honeypot Configuration'), @@ -138,36 +110,48 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#type' => 'checkbox', '#title' => $this->t('Protect all forms with Honeypot'), '#description' => $this->t('Enable Honeypot protection for ALL forms on this site (it is best to only enable Honeypot for the forms you need below).'), - '#default_value' => $this->config('honeypot.settings')->get('protect_all_forms'), + '#default_value' => $honeypot_config->get('protect_all_forms'), ]; $form['configuration']['protect_all_forms']['#description'] .= '<br />' . $this->t('<strong>Page caching will be disabled on any page where a form is present if the Honeypot time limit is not set to 0.</strong>'); $form['configuration']['log'] = [ '#type' => 'checkbox', '#title' => $this->t('Log blocked form submissions'), '#description' => $this->t('Log submissions that are blocked due to Honeypot protection.'), - '#default_value' => $this->config('honeypot.settings')->get('log'), + '#default_value' => $honeypot_config->get('log'), ]; $form['configuration']['element_name'] = [ '#type' => 'textfield', '#title' => $this->t('Honeypot element name'), - '#description' => $this->t("The name of the Honeypot form field. It's usually most effective to use a generic name like email, homepage, or link, but this should be changed if it interferes with fields that are already in your forms. Must not contain spaces or special characters."), - '#default_value' => $this->config('honeypot.settings')->get('element_name'), + '#description' => $this->t("The name of the Honeypot form field. It's usually most effective to use a generic name like email, homepage, or link, but this should be changed if it interferes with fields that are already in your forms. Must not contain spaces or special characters. Should not contain words like 'honeypot', as these may allow a spam bot to identify the purpose of this field."), + '#default_value' => $honeypot_config->get('element_name'), '#required' => TRUE, '#size' => 30, ]; $form['configuration']['time_limit'] = [ - '#type' => 'textfield', + '#type' => 'number', '#title' => $this->t('Honeypot time limit'), '#description' => $this->t('Minimum time required before form should be considered entered by a human instead of a bot. Set to 0 to disable.'), - '#default_value' => $this->config('honeypot.settings')->get('time_limit'), + '#default_value' => $honeypot_config->get('time_limit'), '#required' => TRUE, + '#min' => 0, '#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>'); + $form['configuration']['expire'] = [ + '#type' => 'number', + '#title' => $this->t('Honeypot expire'), + '#description' => $this->t("Entries in the {honeypot_user} table that are older than the value of 'expire' will be deleted when cron is run."), + '#default_value' => $honeypot_config->get('expire'), + '#required' => TRUE, + '#min' => 0, + '#size' => 5, + '#field_suffix' => $this->t('seconds'), + ]; + // Honeypot Enabled forms. - $form_settings = $this->config('honeypot.settings')->get('form_settings'); + $form_settings = $honeypot_config->get('form_settings'); $form['form_settings'] = [ '#type' => 'fieldset', '#title' => $this->t('Honeypot Enabled Forms'), @@ -222,7 +206,8 @@ public function buildForm(array $form, FormStateInterface $form_state) { // Node types for node forms. if ($this->moduleHandler->moduleExists('node')) { - $types = NodeType::loadMultiple(); + /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface[] $types */ + $types = $this->entityTypeManager->getStorage('node_type')->loadMultiple(); if (!empty($types)) { // Node forms. $form['form_settings']['node_forms'] = ['#markup' => '<h5>' . $this->t('Node Forms') . '</h5>']; @@ -239,7 +224,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { // Comment types for comment forms. if ($this->moduleHandler->moduleExists('comment')) { - $types = CommentType::loadMultiple(); + $types = $this->entityTypeManager->getStorage('comment_type')->loadMultiple(); if (!empty($types)) { $form['form_settings']['comment_forms'] = ['#markup' => '<h5>' . $this->t('Comment Forms') . '</h5>']; foreach ($types as $type) { @@ -262,35 +247,13 @@ public function buildForm(array $form, FormStateInterface $form_state) { } $form_state->setStorage(['keys' => $keys_to_save]); - // For now, manually add submit button. Hopefully, by the time D8 is - // released, there will be something like system_settings_form() in D7. - $form['actions']['#type'] = 'container'; - $form['actions']['submit'] = [ - '#type' => 'submit', - '#value' => $this->t('Save configuration'), - ]; - - return $form; + return parent::buildForm($form, $form_state); } /** * {@inheritdoc} */ public function validateForm(array &$form, FormStateInterface $form_state) { - // Make sure the time limit is a positive integer or 0. - $time_limit = $form_state->getValue('time_limit'); - if ((is_numeric($time_limit) && $time_limit > 0) || $time_limit === '0') { - if (ctype_digit($time_limit)) { - // Good to go. - } - else { - $form_state->setErrorByName('time_limit', $this->t("The time limit must be a positive integer or 0.")); - } - } - else { - $form_state->setErrorByName('time_limit', $this->t("The time limit must be a positive integer or 0.")); - } - // Make sure Honeypot element name only contains A-Z, 0-9. if (!preg_match("/^[-_a-zA-Z0-9]+$/", $form_state->getValue('element_name'))) { $form_state->setErrorByName('element_name', $this->t("The element name cannot contain spaces or other special characters.")); @@ -334,8 +297,32 @@ public function submitForm(array &$form, FormStateInterface $form_state) { // Clear the honeypot protected forms cache. $this->cache->delete('honeypot_protected_forms'); - // Tell the user the settings have been saved. - $this->messenger->addMessage($this->t('The configuration options have been saved.')); + parent::submitForm($form, $form_state); + } + + /** + * Gets a value from the retrieved form settings array. + * + * @param array $form_settings + * Array of configuration settings that Honeypot uses to determine which + * forms to protect. Keys are form IDs, values are boolean to indicate + * whether that form ID should be protected. + * @param string $form_id + * The Form ID. + * + * @return bool + * TRUE if the form is protected. + */ + protected function getFormSettingsValue(array $form_settings, string $form_id) { + // If there are settings in the array and the form ID already has a setting, + // return the saved setting for the form ID. + if (!empty($form_settings) && isset($form_settings[$form_id])) { + return $form_settings[$form_id]; + } + // Default to false. + else { + return FALSE; + } } } diff --git a/web/modules/honeypot/src/HoneypotService.php b/web/modules/honeypot/src/HoneypotService.php new file mode 100644 index 0000000000000000000000000000000000000000..c2b72dc0bd63b8e146661c6b90002e01fc5aa16c --- /dev/null +++ b/web/modules/honeypot/src/HoneypotService.php @@ -0,0 +1,388 @@ +<?php + +namespace Drupal\honeypot; + +use Drupal\Component\Datetime\TimeInterface; +use Drupal\Component\Utility\Crypt; +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Config\ConfigFactory; +use Drupal\Core\Database\Connection; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\KeyValueStore\KeyValueExpirableFactory; +use Drupal\Core\Logger\LoggerChannelFactoryInterface; +use Drupal\Core\PageCache\ResponsePolicy\KillSwitch; +use Drupal\Core\Session\AccountProxyInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\StringTranslation\TranslationInterface; +use Symfony\Component\HttpFoundation\RequestStack; + +/** + * Provides a service to append Honeypot protection to forms. + */ +class HoneypotService implements HoneypotServiceInterface { + use StringTranslationTrait; + + /** + * Drupal account object. + * + * @var \Drupal\Core\Session\AccountProxyInterface + */ + protected $account; + + /** + * The module_handler service. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** + * Drupal configuration object factory service. + * + * @var \Drupal\Core\Config\ImmutableConfig + */ + protected $config; + + /** + * Drupal key value factory store factory. + * + * @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface + */ + protected $keyValue; + + /** + * Killswitch policy object. + * + * @var \Drupal\Core\PageCache\ResponsePolicy\KillSwitch + */ + protected $killSwitch; + + /** + * The database connection to use. + * + * @var \Drupal\Core\Database\Connection + */ + protected $connection; + + /** + * The Honeypot logger channel. + * + * @var \Drupal\Core\Logger\LoggerChannelInterface + */ + protected $logger; + + /** + * The datetime.time service. + * + * @var \Drupal\Component\Datetime\TimeInterface + */ + protected $timeService; + + /** + * A cache backend interface. + * + * @var \Drupal\Core\Cache\CacheBackendInterface + */ + protected $cacheBackend; + + /** + * The request stack service. + * + * @var \Symfony\Component\HttpFoundation\RequestStack + */ + protected $requestStack; + + /** + * HoneypotService constructor. + * + * @param \Drupal\Core\Session\AccountProxyInterface $account + * Drupal account object. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module_handler service. + * @param \Drupal\Core\Config\ConfigFactory $config_factory + * Drupal configuration object factory service. + * @param \Drupal\Core\KeyValueStore\KeyValueExpirableFactory $key_value + * Drupal key value factory store factory. + * @param \Drupal\Core\PageCache\ResponsePolicy\KillSwitch $kill_switch + * Killswitch policy object. + * @param \Drupal\Core\Database\Connection $connection + * The database connection. + * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory + * The logger.factory service. + * @param \Drupal\Component\Datetime\TimeInterface $time_service + * The datetime.time service. + * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation + * The string translation service. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend + * The cache backend interface. + * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack + * The request stack service. + */ + public function __construct(AccountProxyInterface $account, ModuleHandlerInterface $module_handler, ConfigFactory $config_factory, KeyValueExpirableFactory $key_value, KillSwitch $kill_switch, Connection $connection, LoggerChannelFactoryInterface $logger_factory, TimeInterface $time_service, TranslationInterface $string_translation, CacheBackendInterface $cache_backend, RequestStack $request_stack) { + $this->account = $account; + $this->moduleHandler = $module_handler; + $this->config = $config_factory->get('honeypot.settings'); + $this->keyValue = $key_value->get('honeypot_time_restriction'); + $this->killSwitch = $kill_switch; + $this->connection = $connection; + $this->logger = $logger_factory->get('honeypot'); + $this->timeService = $time_service; + $this->stringTranslation = $string_translation; + $this->cacheBackend = $cache_backend; + $this->requestStack = $request_stack; + } + + /** + * {@inheritdoc} + */ + public function getProtectedForms(): array { + $forms = &drupal_static(__METHOD__); + + // If the data isn't already in memory, get from cache or look it up fresh. + if (!isset($forms)) { + if ($cache = $this->cacheBackend->get('honeypot_protected_forms')) { + $forms = $cache->data; + } + else { + $forms = []; + $form_settings = $this->config->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; + } + } + } + + // Save the cached data. + $this->cacheBackend->set('honeypot_protected_forms', $forms); + } + } + + return $forms; + } + + /** + * {@inheritdoc} + */ + public function getTimeLimit(array $form_values = []): int { + $honeypot_time_limit = $this->config->get('time_limit'); + + // Only calculate time limit if honeypot_time_limit has a value > 0. + if ($honeypot_time_limit > 0) { + $expire_time = $this->config->get('expire'); + + // Query the {honeypot_user} table to determine the number of failed + // submissions for the current user. + $uid = $this->account->id(); + $query = $this->connection->select('honeypot_user', 'hu') + ->condition('uid', $uid) + ->condition('timestamp', $this->timeService->getRequestTime() - $expire_time, '>'); + + // For anonymous users, take the hostname into account. + if ($uid === 0) { + $hostname = $this->requestStack->getCurrentRequest()->getClientIp(); + $query->condition('hostname', $hostname); + } + $number = $query->countQuery()->execute()->fetchField(); + + // Don't add more time than the expiration window. + $honeypot_time_limit = (int) min($honeypot_time_limit + exp($number) - 1, $expire_time); + // @todo Only accepts two args. + $additions = $this->moduleHandler->invokeAll('honeypot_time_limit', [ + $honeypot_time_limit, + $form_values, + $number, + ]); + if (count($additions)) { + $honeypot_time_limit += array_sum($additions); + } + } + + return $honeypot_time_limit; + } + + /** + * {@inheritdoc} + */ + public function addFormProtection(array &$form, FormStateInterface $form_state, array $options = []): void { + // Allow other modules to alter the protections applied to this form. + $this->moduleHandler->alter('honeypot_form_protections', $options, $form); + + // Don't add any protections if the user can bypass the Honeypot. + if ($this->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 = $this->config->get('element_name'); + + // Build the honeypot element. + $honeypot_class = $honeypot_element . '-textfield'; + $form[$honeypot_element] = [ + '#theme_wrappers' => [ + 0 => 'form_element', + 'container' => [ + '#id' => NULL, + '#attributes' => [ + 'class' => [$honeypot_class], + 'style' => ['display: none !important;'], + ], + ], + ], + '#type' => 'textfield', + '#title' => $this->t('Leave this field blank'), + '#size' => 20, + '#weight' => 1, + '#attributes' => ['autocomplete' => 'off'], + '#element_validate' => [ + [$this, 'validateHoneypot'], + ], + ]; + + } + + // Set the time restriction for this form (if it's not disabled). + if (in_array('time_restriction', $options) && $this->config->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(); + $this->keyValue->setWithExpire($identifier, $this->timeService->getCurrentTime(), 3600 * 24); + } + else { + $identifier = $input['honeypot_time']; + } + $form['honeypot_time'] = [ + '#type' => 'hidden', + '#title' => $this->t('Timestamp'), + '#default_value' => $identifier, + '#element_validate' => [ + [$this, 'validateTimeRestriction'], + ], + '#cache' => ['max-age' => 0], + ]; + + // 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(); + } + } + + // Allow other modules to react to addition of form protection. + if (!empty($options)) { + $this->moduleHandler->invokeAll('honeypot_add_form_protection', [$options, $form]); + } + } + + /** + * An #element_validate callback for the honeypot field. + * + * @param array $element + * An associative array containing the properties and children of the + * generic form element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param array $complete_form + * The complete form structure. + */ + public function validateHoneypot(array &$element, FormStateInterface $form_state, array &$complete_form): void { + // Get the honeypot field value. + $honeypot_value = $element['#value']; + + // Make sure it's empty. + if (!empty($honeypot_value) || $honeypot_value == '0') { + $this->log($form_state->getValue('form_id'), 'honeypot'); + $form_state->setErrorByName('', $this->t('There was a problem with your form submission. Please refresh the page and try again.')); + } + } + + /** + * An #element_validate callback for the honeypot time restriction field. + * + * @param array $element + * An associative array containing the properties and children of the + * generic form element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param array $complete_form + * The complete form structure. + */ + public function validateTimeRestriction(array &$element, FormStateInterface $form_state, array &$complete_form): void { + 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'] == $this->t('Preview')) { + return; + } + + // Get the time value. + $identifier = $form_state->getValue('honeypot_time', FALSE); + $honeypot_time = $this->keyValue->get($identifier, 0); + + // Get the honeypot_time_limit. + $time_limit = $this->getTimeLimit($form_state->getValues()); + + // Make sure current time - (time_limit + form time value) is greater + // than 0. If not, throw an error. + if (!$honeypot_time || $this->timeService->getRequestTime() < ($honeypot_time + $time_limit)) { + $this->log($form_state->getValue('form_id'), 'honeypot_time'); + $time_limit = $this->getTimeLimit(); + $this->keyValue->setWithExpire($identifier, $this->timeService->getRequestTime(), 3600 * 24); + $form_state->setErrorByName('', $this->t('There was a problem with your form submission. Please wait @limit seconds and try again.', ['@limit' => $time_limit])); + } + } + + /** + * Logs 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. + */ + protected function log(string $form_id, string $type): void { + $this->logFailure($form_id, $type); + if ($this->config->get('log')) { + $variables = [ + '%form' => $form_id, + '@cause' => ($type == 'honeypot') ? $this->t('submission of a value in the honeypot field') : $this->t('submission of the form in less than minimum required time'), + ]; + $this->logger->notice('Blocked submission of %form due to @cause.', $variables); + } + } + + /** + * {@inheritdoc} + */ + public function logFailure(string $form_id, string $type): void { + $uid = $this->account->id(); + + // Log failed submissions. + $this->connection->insert('honeypot_user') + ->fields([ + 'uid' => $uid, + 'hostname' => $this->requestStack->getCurrentRequest()->getClientIp(), + 'timestamp' => $this->timeService->getRequestTime(), + ]) + ->execute(); + + // Allow other modules to react to honeypot rejections. + // @todo Only accepts two args. + $this->moduleHandler->invokeAll('honeypot_reject', [$form_id, $uid, $type]); + } + +} diff --git a/web/modules/honeypot/src/HoneypotServiceInterface.php b/web/modules/honeypot/src/HoneypotServiceInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..d0bcd567e8454dddd3d0ed0f6cae408533ed0bcc --- /dev/null +++ b/web/modules/honeypot/src/HoneypotServiceInterface.php @@ -0,0 +1,57 @@ +<?php + +namespace Drupal\honeypot; + +use Drupal\Core\Form\FormStateInterface; + +/** + * Provides a service to append Honeypot protection to forms. + */ +interface HoneypotServiceInterface { + + /** + * Builds an array of all the protected forms on the site. + * + * @return array + * An array whose values are the form_ids of all the protected forms + * on the site. + */ + public function getProtectedForms(): array; + + /** + * Looks up the time limit for the current user. + * + * @param array $form_values + * (optional) Array of form values. + * + * @return int + * The time limit in seconds. + */ + public function getTimeLimit(array $form_values = []): int; + + /** + * Adds honeypot protection to provided form. + * + * @param array $form + * Drupal form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Drupal form state object. + * @param array $options + * (optional) Array of options to be added to form. Currently accepts + * 'honeypot' and 'time_restriction'. + */ + public function addFormProtection(array &$form, FormStateInterface $form_state, array $options = []): void; + + /** + * Logs 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. + */ + public function logFailure(string $form_id, string $type): void; + +} diff --git a/web/modules/honeypot/tests/modules/honeypot_test/honeypot_test.info.yml b/web/modules/honeypot/tests/modules/honeypot_test/honeypot_test.info.yml index 0e0777c6118e249f791b4a2dca9bf1b08c3212d8..68ba249c2c8f38d294c4aa765990c05fd9e1a0b9 100644 --- a/web/modules/honeypot/tests/modules/honeypot_test/honeypot_test.info.yml +++ b/web/modules/honeypot/tests/modules/honeypot_test/honeypot_test.info.yml @@ -3,7 +3,7 @@ type: module description: Support module for Honeypot internal testing purposes. package: Testing -# Information added by Drupal.org packaging script on 2020-08-19 -version: '2.0.1' +# Information added by Drupal.org packaging script on 2022-05-07 +version: '2.1.0' project: 'honeypot' -datestamp: 1597855130 +datestamp: 1651894956 diff --git a/web/modules/honeypot/tests/modules/honeypot_test/src/Controller/HoneypotTestController.php b/web/modules/honeypot/tests/modules/honeypot_test/src/Controller/HoneypotTestController.php index 56844615b273d644c49ac46ec83abd69b201f079..a9d2e9f47cd094292a86bf9b98dc6ae4b744cd6e 100644 --- a/web/modules/honeypot/tests/modules/honeypot_test/src/Controller/HoneypotTestController.php +++ b/web/modules/honeypot/tests/modules/honeypot_test/src/Controller/HoneypotTestController.php @@ -2,16 +2,42 @@ namespace Drupal\honeypot_test\Controller; +use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\Core\Form\FormBuilderInterface; use Drupal\Core\Form\FormState; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\JsonResponse; -use Drupal\Core\StringTranslation\StringTranslationTrait; /** * Controller for honeypot_test routes. */ -class HoneypotTestController { +class HoneypotTestController implements ContainerInjectionInterface { - use StringTranslationTrait; + /** + * The form_builder service. + * + * @var \Drupal\Core\Form\FormBuilderInterface + */ + protected $formBuilder; + + /** + * Constructs a HoneypotTestController. + * + * @param \Drupal\Core\Form\FormBuilderInterface $form_builder + * The form builder service. + */ + public function __construct(FormBuilderInterface $form_builder) { + $this->formBuilder = $form_builder; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('form_builder') + ); + } /** * Page that triggers a programmatic form submission. @@ -23,10 +49,10 @@ public function submitFormPage() { $values = [ 'name' => 'robo-user', 'mail' => 'robouser@example.com', - 'op' => $this->t('Submit'), + 'op' => 'Submit', ]; $form_state->setValues($values); - \Drupal::formBuilder()->submitForm('\Drupal\user\Form\UserPasswordForm', $form_state); + $this->formBuilder->submitForm('\Drupal\user\Form\UserPasswordForm', $form_state); return new JsonResponse($form_state->getErrors()); } diff --git a/web/modules/honeypot/tests/src/Functional/HoneypotAdminFormTest.php b/web/modules/honeypot/tests/src/Functional/HoneypotAdminFormTest.php index e46d2b7800e0b7d64ac52b27ad1b9e1f020a02b0..6b9cc7a46398355172dacd381ebd97446ab7b6c7 100644 --- a/web/modules/honeypot/tests/src/Functional/HoneypotAdminFormTest.php +++ b/web/modules/honeypot/tests/src/Functional/HoneypotAdminFormTest.php @@ -3,7 +3,6 @@ namespace Drupal\Tests\honeypot\Functional; use Drupal\Tests\BrowserTestBase; -use Drupal\Core\StringTranslation\StringTranslationTrait; /** * Test Honeypot spam protection admin form functionality. @@ -12,8 +11,6 @@ */ class HoneypotAdminFormTest extends BrowserTestBase { - use StringTranslationTrait; - /** * Admin user. * @@ -22,24 +19,19 @@ class HoneypotAdminFormTest extends BrowserTestBase { protected $adminUser; /** - * Default theme. - * - * @var string + * {@inheritdoc} */ protected $defaultTheme = 'stark'; /** - * Modules to enable. - * - * @var array + * {@inheritdoc} */ - public static $modules = ['honeypot']; + protected static $modules = ['honeypot']; /** - * Setup before test. + * {@inheritdoc} */ - public function setUp() { - // Enable modules required for this test. + protected function setUp(): void { parent::setUp(); // Set up admin user. @@ -52,35 +44,41 @@ public function setUp() { /** * Test a valid element name. */ - public function testElementNameUpdateSuccess() { + public function testElementNameUpdateSuccess(): void { + /** @var \Drupal\Tests\WebAssert $assert */ + $assert = $this->assertSession(); + // Log in the admin user. $this->drupalLogin($this->adminUser); // Set up form and submit it. $edit['element_name'] = "test"; - $this->drupalPostForm('admin/config/content/honeypot', $edit, $this->t('Save configuration')); + $this->drupalGet('admin/config/content/honeypot'); + $this->submitForm($edit, 'Save configuration'); // Form should have been submitted successfully. - $this->assertSession()->pageTextContains('The configuration options have been saved.'); + $assert->pageTextContains('The configuration options have been saved.'); // Set up form and submit it. $edit['element_name'] = "test-1"; - $this->drupalPostForm('admin/config/content/honeypot', $edit, $this->t('Save configuration')); + $this->drupalGet('admin/config/content/honeypot'); + $this->submitForm($edit, 'Save configuration'); // Form should have been submitted successfully. - $this->assertSession()->pageTextContains('The configuration options have been saved.'); + $assert->pageTextContains('The configuration options have been saved.'); } /** * Test an invalid element name (invalid first character). */ - public function testElementNameUpdateFirstCharacterFail() { + public function testElementNameUpdateFirstCharacterFail(): void { // Log in the admin user. $this->drupalLogin($this->adminUser); // Set up form and submit it. $edit['element_name'] = "1test"; - $this->drupalPostForm('admin/config/content/honeypot', $edit, $this->t('Save configuration')); + $this->drupalGet('admin/config/content/honeypot'); + $this->submitForm($edit, 'Save configuration'); // Form submission should fail. $this->assertSession()->pageTextContains('The element name must start with a letter.'); @@ -89,23 +87,28 @@ public function testElementNameUpdateFirstCharacterFail() { /** * Test an invalid element name (invalid character in name). */ - public function testElementNameUpdateInvalidCharacterFail() { + public function testElementNameUpdateInvalidCharacterFail(): void { + /** @var \Drupal\Tests\WebAssert $assert */ + $assert = $this->assertSession(); + // Log in the admin user. $this->drupalLogin($this->adminUser); // Set up form and submit it. $edit['element_name'] = "special-character-&"; - $this->drupalPostForm('admin/config/content/honeypot', $edit, $this->t('Save configuration')); + $this->drupalGet('admin/config/content/honeypot'); + $this->submitForm($edit, 'Save configuration'); // Form submission should fail. - $this->assertSession()->pageTextContains('The element name cannot contain spaces or other special characters.'); + $assert->pageTextContains('The element name cannot contain spaces or other special characters.'); // Set up form and submit it. $edit['element_name'] = "space in name"; - $this->drupalPostForm('admin/config/content/honeypot', $edit, $this->t('Save configuration')); + $this->drupalGet('admin/config/content/honeypot'); + $this->submitForm($edit, 'Save configuration'); // Form submission should fail. - $this->assertSession()->pageTextContains('The element name cannot contain spaces or other special characters.'); + $assert->pageTextContains('The element name cannot contain spaces or other special characters.'); } } diff --git a/web/modules/honeypot/tests/src/Functional/HoneypotFormCacheTest.php b/web/modules/honeypot/tests/src/Functional/HoneypotFormCacheTest.php index 69ccb5d1a943957158c908cb557c2f9aed5d0380..1c3ed6b73da663df78a444da342b633c20447518 100644 --- a/web/modules/honeypot/tests/src/Functional/HoneypotFormCacheTest.php +++ b/web/modules/honeypot/tests/src/Functional/HoneypotFormCacheTest.php @@ -16,13 +16,10 @@ * @group honeypot */ class HoneypotFormCacheTest extends BrowserTestBase { - use CommentTestTrait; /** - * Default theme. - * - * @var string + * {@inheritdoc} */ protected $defaultTheme = 'stark'; @@ -34,16 +31,14 @@ class HoneypotFormCacheTest extends BrowserTestBase { protected $node; /** - * Modules to enable. - * - * @var array + * {@inheritdoc} */ - public static $modules = ['honeypot', 'node', 'comment', 'contact']; + protected static $modules = ['honeypot', 'node', 'comment', 'contact']; /** * {@inheritdoc} */ - protected function setUp() { + protected function setUp(): void { parent::setUp(); // Set up required Honeypot configuration. @@ -73,7 +68,10 @@ protected function setUp() { /** * Test enabling and disabling of page cache based on time limit settings. */ - public function testCacheContactForm() { + public function testCacheContactForm(): void { + /** @var \Drupal\Tests\WebAssert $assert */ + $assert = $this->assertSession(); + // Create a Website feedback contact form. $feedback_form = ContactForm::create([ 'id' => 'feedback', @@ -96,7 +94,7 @@ public function testCacheContactForm() { // Test on cache header with time limit enabled, cache should miss. $this->drupalGet('contact/feedback'); - $this->assertEquals('', $this->drupalGetHeader('X-Drupal-Cache'), 'Page was not cached.'); + $assert->responseHeaderEquals('X-Drupal-Cache', NULL); // Disable time limit. \Drupal::configFactory()->getEditable('honeypot.settings')->set('time_limit', 0)->save(); @@ -105,18 +103,21 @@ public function testCacheContactForm() { $this->drupalGet('contact/feedback'); // Test on cache header with time limit disabled, cache should hit. $this->drupalGet('contact/feedback'); - $this->assertEquals('HIT', $this->drupalGetHeader('X-Drupal-Cache'), 'Page was cached.'); + $assert->responseHeaderEquals('X-Drupal-Cache', 'HIT'); // Re-enable the time limit, we should not be seeing the cached version. \Drupal::configFactory()->getEditable('honeypot.settings')->set('time_limit', 5)->save(); $this->drupalGet('contact/feedback'); - $this->assertEquals('', $this->drupalGetHeader('X-Drupal-Cache'), 'Page was not cached.'); + $assert->responseHeaderEquals('X-Drupal-Cache', NULL); } /** * Test enabling and disabling of page cache based on time limit settings. */ - public function testCacheCommentForm() { + public function testCacheCommentForm(): void { + /** @var \Drupal\Tests\WebAssert $assert */ + $assert = $this->assertSession(); + // Set up example node. $this->node = $this->drupalCreateNode([ 'type' => 'article', @@ -134,7 +135,7 @@ public function testCacheCommentForm() { // Test on cache header with time limit enabled, cache should miss. $this->drupalGet('node/' . $this->node->id()); - $this->assertEquals('', $this->drupalGetHeader('X-Drupal-Cache'), 'Page was not cached.'); + $assert->responseHeaderEquals('X-Drupal-Cache', NULL); // Disable time limit. \Drupal::configFactory()->getEditable('honeypot.settings')->set('time_limit', 0)->save(); @@ -144,8 +145,7 @@ public function testCacheCommentForm() { // Test on cache header with time limit disabled, cache should hit. $this->drupalGet('node/' . $this->node->id()); - $this->assertEquals('HIT', $this->drupalGetHeader('X-Drupal-Cache'), 'Page was cached.'); - + $assert->responseHeaderEquals('X-Drupal-Cache', 'HIT'); } } diff --git a/web/modules/honeypot/tests/src/Functional/HoneypotFormProgrammaticSubmissionTest.php b/web/modules/honeypot/tests/src/Functional/HoneypotFormProgrammaticSubmissionTest.php index 50996ecc3cedef548223aa3a34848198cc9062c5..256e306d07f1ce579d236e61a286dc38dedd1c3c 100644 --- a/web/modules/honeypot/tests/src/Functional/HoneypotFormProgrammaticSubmissionTest.php +++ b/web/modules/honeypot/tests/src/Functional/HoneypotFormProgrammaticSubmissionTest.php @@ -13,23 +13,19 @@ class HoneypotFormProgrammaticSubmissionTest extends BrowserTestBase { /** - * Default theme. - * - * @var string + * {@inheritdoc} */ protected $defaultTheme = 'stark'; /** - * Modules to enable. - * - * @var array + * {@inheritdoc} */ - public static $modules = ['honeypot', 'honeypot_test', 'user']; + protected static $modules = ['honeypot', 'honeypot_test', 'user']; /** * {@inheritdoc} */ - protected function setUp() { + protected function setUp(): void { parent::setUp(); // Set up required Honeypot configuration. @@ -46,7 +42,7 @@ protected function setUp() { /** * Trigger a programmatic form submission and verify the validation errors. */ - public function testProgrammaticFormSubmission() { + public function testProgrammaticFormSubmission(): void { $result = $this->drupalGet('/honeypot_test/submit_form'); $form_errors = (array) Json::decode($result); $this->assertSession()->responseNotContains('There was a problem with your form submission. Please wait 6 seconds and try again.'); diff --git a/web/modules/honeypot/tests/src/Functional/HoneypotFormTest.php b/web/modules/honeypot/tests/src/Functional/HoneypotFormTest.php index 0477d50d4650b0462cad232a6d7ef95be41222ee..fe3483905b44b12348d9f716806601a7d17016dc 100644 --- a/web/modules/honeypot/tests/src/Functional/HoneypotFormTest.php +++ b/web/modules/honeypot/tests/src/Functional/HoneypotFormTest.php @@ -5,7 +5,6 @@ use Drupal\comment\Tests\CommentTestTrait; use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface; use Drupal\contact\Entity\ContactForm; -use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Tests\BrowserTestBase; use Drupal\user\UserInterface; @@ -15,9 +14,7 @@ * @group honeypot */ class HoneypotFormTest extends BrowserTestBase { - use CommentTestTrait; - use StringTranslationTrait; /** * Admin user. @@ -41,24 +38,19 @@ class HoneypotFormTest extends BrowserTestBase { protected $node; /** - * Default theme. - * - * @var string + * {@inheritdoc} */ protected $defaultTheme = 'stark'; /** - * Modules to enable. - * - * @var array + * {@inheritdoc} */ - public static $modules = ['honeypot', 'node', 'comment', 'contact']; + protected static $modules = ['honeypot', 'node', 'comment', 'contact']; /** * {@inheritdoc} */ - public function setUp() { - // Enable modules required for this test. + protected function setUp(): void { parent::setUp(); // Set up required Honeypot configuration. @@ -114,7 +106,7 @@ public function setUp() { /** * Make sure user login form is not protected. */ - public function testUserLoginNotProtected() { + public function testUserLoginNotProtected(): void { $this->drupalGet('user'); $this->assertSession()->responseNotContains('id="edit-url" name="url"'); } @@ -122,11 +114,12 @@ public function testUserLoginNotProtected() { /** * Test user registration (anonymous users). */ - public function testProtectRegisterUserNormal() { + public function testProtectRegisterUserNormal(): void { // Set up form and submit it. $edit['name'] = $this->randomMachineName(); $edit['mail'] = $edit['name'] . '@example.com'; - $this->drupalPostForm('user/register', $edit, $this->t('Create new account')); + $this->drupalGet('user/register'); + $this->submitForm($edit, 'Create new account'); // Form should have been submitted successfully. $this->assertSession()->pageTextContains('A welcome message with further instructions has been sent to your email address.'); @@ -135,12 +128,13 @@ public function testProtectRegisterUserNormal() { /** * Test for user register honeypot filled. */ - public function testProtectUserRegisterHoneypotFilled() { + public function testProtectUserRegisterHoneypotFilled(): void { // Set up form and submit it. $edit['name'] = $this->randomMachineName(); $edit['mail'] = $edit['name'] . '@example.com'; $edit['url'] = 'http://www.example.com/'; - $this->drupalPostForm('user/register', $edit, $this->t('Create new account')); + $this->drupalGet('user/register'); + $this->submitForm($edit, 'Create new account'); // Form should have error message. $this->assertSession()->pageTextContains('There was a problem with your form submission. Please refresh the page and try again.'); @@ -149,7 +143,11 @@ public function testProtectUserRegisterHoneypotFilled() { /** * Test for user register too fast. */ - public function testProtectRegisterUserTooFast() { + public function testProtectRegisterUserTooFast(): void { + /** @var \Drupal\Tests\WebAssert $assert */ + $assert = $this->assertSession(); + + // Set the time limit to 1 second. \Drupal::configFactory()->getEditable('honeypot.settings')->set('time_limit', 1)->save(); // First attempt a submission that does not trigger honeypot. @@ -157,8 +155,8 @@ public function testProtectRegisterUserTooFast() { $edit['mail'] = $edit['name'] . '@example.com'; $this->drupalGet('user/register'); sleep(2); - $this->drupalPostForm(NULL, $edit, $this->t('Create new account')); - $this->assertNoText($this->t('There was a problem with your form submission.')); + $this->submitForm($edit, 'Create new account'); + $assert->pageTextNotContains('There was a problem with your form submission.'); // Set the time limit a bit higher so we can trigger honeypot. \Drupal::configFactory()->getEditable('honeypot.settings')->set('time_limit', 5)->save(); @@ -166,7 +164,8 @@ public function testProtectRegisterUserTooFast() { // Set up form and submit it. $edit['name'] = $this->randomMachineName(); $edit['mail'] = $edit['name'] . '@example.com'; - $this->drupalPostForm('user/register', $edit, $this->t('Create new account')); + $this->drupalGet('user/register'); + $this->submitForm($edit, 'Create new account'); // Form should have error message. $this->assertSession()->pageTextContains('There was a problem with your form submission. Please wait 6 seconds and try again.'); @@ -175,7 +174,7 @@ public function testProtectRegisterUserTooFast() { /** * Test that any (not-strict-empty) value triggers protection. */ - public function testStrictEmptinessOnHoneypotField() { + public function testStrictEmptinessOnHoneypotField(): void { // Initialise the form values. $edit['name'] = $this->randomMachineName(); $edit['mail'] = $edit['name'] . '@example.com'; @@ -183,15 +182,18 @@ public function testStrictEmptinessOnHoneypotField() { // Any value that is not strictly empty should trigger Honeypot. foreach (['0', ' '] as $value) { $edit['url'] = $value; - $this->drupalPostForm('user/register', $edit, $this->t('Create new account')); - $this->assertText($this->t('There was a problem with your form submission. Please refresh the page and try again.'), "Honeypot protection is triggered when the honeypot field contains '{$value}'."); + $this->drupalGet('user/register'); + $this->submitForm($edit, 'Create new account'); + // Assert that Honeypot protection is triggered when the honeypot field + // contains $value. + $this->assertSession()->pageTextContains('There was a problem with your form submission. Please refresh the page and try again.'); } } /** * Test comment form protection. */ - public function testProtectCommentFormNormal() { + public function testProtectCommentFormNormal(): void { $comment = 'Test comment.'; // Disable time limit for honeypot. @@ -202,14 +204,15 @@ public function testProtectCommentFormNormal() { // Set up form and submit it. $edit["comment_body[0][value]"] = $comment; - $this->drupalPostForm('comment/reply/node/' . $this->node->id() . '/comment', $edit, $this->t('Save')); + $this->drupalGet('comment/reply/node/' . $this->node->id() . '/comment'); + $this->submitForm($edit, 'Save'); $this->assertSession()->pageTextContains('Your comment has been queued for review'); } /** * Test for comment form honeypot filled. */ - public function testProtectCommentFormHoneypotFilled() { + public function testProtectCommentFormHoneypotFilled(): void { $comment = 'Test comment.'; // Log in the web user. @@ -218,14 +221,15 @@ public function testProtectCommentFormHoneypotFilled() { // Set up form and submit it. $edit["comment_body[0][value]"] = $comment; $edit['url'] = 'http://www.example.com/'; - $this->drupalPostForm('comment/reply/node/' . $this->node->id() . '/comment', $edit, $this->t('Save')); + $this->drupalGet('comment/reply/node/' . $this->node->id() . '/comment'); + $this->submitForm($edit, 'Save'); $this->assertSession()->pageTextContains('There was a problem with your form submission. Please refresh the page and try again.'); } /** * Test for comment form honeypot bypass. */ - public function testProtectCommentFormHoneypotBypass() { + public function testProtectCommentFormHoneypotBypass(): void { // Log in the admin user. $this->drupalLogin($this->adminUser); @@ -237,7 +241,7 @@ public function testProtectCommentFormHoneypotBypass() { /** * Test node form protection. */ - public function testProtectNodeFormTooFast() { + public function testProtectNodeFormTooFast(): void { // Log in the admin user. $this->drupalLogin($this->webUser); @@ -246,27 +250,29 @@ public function testProtectNodeFormTooFast() { // Set up the form and submit it. $edit["title[0][value]"] = 'Test Page'; - $this->drupalPostForm('node/add/article', $edit, $this->t('Save')); + $this->drupalGet('node/add/article'); + $this->submitForm($edit, 'Save'); $this->assertSession()->pageTextContains('There was a problem with your form submission.'); } /** * Test node form protection. */ - public function testProtectNodeFormPreviewPassthru() { + public function testProtectNodeFormPreviewPassthru(): void { // Log in the admin user. $this->drupalLogin($this->webUser); // Post a node form using the 'Preview' button and make sure it's allowed. $edit["title[0][value]"] = 'Test Page'; - $this->drupalPostForm('node/add/article', $edit, $this->t('Preview')); + $this->drupalGet('node/add/article'); + $this->submitForm($edit, 'Preview'); $this->assertSession()->pageTextNotContains('There was a problem with your form submission.'); } /** * Test protection on the Contact form. */ - public function testProtectContactForm() { + public function testProtectContactForm(): void { $this->drupalLogin($this->adminUser); // Disable 'protect_all_forms'. @@ -288,9 +294,11 @@ public function testProtectContactForm() { $contact_settings->set('default_form', 'feedback')->save(); // Submit the admin form so we can verify the right forms are displayed. - $this->drupalPostForm('admin/config/content/honeypot', [ - 'form_settings[contact_message_feedback_form]' => TRUE, - ], $this->t('Save configuration')); + $this->drupalGet('admin/config/content/honeypot'); + $this->submitForm( + ['form_settings[contact_message_feedback_form]' => TRUE], + 'Save configuration' + ); $this->drupalLogin($this->webUser); $this->drupalGet('contact/feedback'); diff --git a/web/modules/honeypot/tests/src/Kernel/EventIntegrationTest.php b/web/modules/honeypot/tests/src/Kernel/EventIntegrationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..da490a86147d34e2b612ca6c62700f1c5557fb2b --- /dev/null +++ b/web/modules/honeypot/tests/src/Kernel/EventIntegrationTest.php @@ -0,0 +1,78 @@ +<?php + +namespace Drupal\Tests\honeypot\Kernel; + +use Drupal\Tests\rules\Kernel\RulesKernelTestBase; + +/** + * Tests for the Symfony event mapping to Rules events. + * + * @group honeypot + */ +class EventIntegrationTest extends RulesKernelTestBase { + + /** + * The entity storage for Rules config entities. + * + * @var \Drupal\Core\Entity\EntityStorageInterface + */ + protected $storage; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'honeypot', + 'rules', + 'typed_data', + 'field', + 'node', + 'text', + 'user', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->storage = $this->container->get('entity_type.manager')->getStorage('rules_reaction_rule'); + + $this->installEntitySchema('user'); + $this->installEntitySchema('node'); + + $this->installConfig(['system']); + $this->installConfig(['field']); + $this->installConfig(['node']); + $this->installSchema('node', ['node_access']); + $this->installSchema('system', ['sequences']); + } + + /** + * Tests that rejecting a form submission triggers the Rules event listener. + */ + public function testHoneypotRejectEvent(): void { + $rule = $this->expressionManager->createRule(); + $rule->addCondition('rules_test_true'); + $rule->addAction('rules_test_debug_log'); + + $config_entity = $this->storage->create([ + 'id' => 'test_rule', + 'events' => [['event_name' => 'honeypot.form_submission_rejected']], + 'expression' => $rule->getConfiguration(), + ]); + $config_entity->save(); + + // The logger instance has changed, refresh it. + $this->logger = $this->container->get('logger.channel.rules_debug'); + $this->logger->addLogger($this->debugLog); + + // Invoke hook_honeypot_reject() manually, which should trigger the rule. + $account = $this->container->get('current_user'); + honeypot_honeypot_reject('test_form_id', $account->id(), 'honeypot'); + + // Test that the action in the rule logged something. + $this->assertRulesDebugLogEntryExists('action called'); + } + +} diff --git a/web/modules/honeypot/tests/src/Kernel/HoneypotLegacyTest.php b/web/modules/honeypot/tests/src/Kernel/HoneypotLegacyTest.php new file mode 100644 index 0000000000000000000000000000000000000000..753dd1849034c163f800b95938287e16107b8b36 --- /dev/null +++ b/web/modules/honeypot/tests/src/Kernel/HoneypotLegacyTest.php @@ -0,0 +1,65 @@ +<?php + +namespace Drupal\Tests\honeypot\Kernel; + +use Drupal\Core\Form\FormState; +use Drupal\KernelTests\KernelTestBase; + +/** + * Tests legacy honeypot functionality. + * + * @group honeypot + * @group legacy + */ +class HoneypotLegacyTest extends KernelTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = ['honeypot', 'user']; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->installEntitySchema('user'); + $this->installSchema('honeypot', ['honeypot_user']); + $this->installConfig(['honeypot']); + } + + /** + * Tests the deprecation message for honeypot_get_protected_forms(). + */ + public function testGetProtectedForms(): void { + $this->expectDeprecation("honeypot_get_protected_forms() is deprecated in honeypot:2.1.0 and is removed from honeypot:3.0.0. Use the 'honeypot' service instead. For example, \Drupal::service('honeypot')->getProtectedForms(). See https://www.drupal.org/node/2949447"); + $this->assertIsArray(honeypot_get_protected_forms()); + } + + /** + * Tests the deprecation message for honeypot_add_form_protection(). + */ + public function testAddFormProtection(): void { + $this->expectDeprecation("honeypot_add_form_protection() is deprecated in honeypot:2.1.0 and is removed from honeypot:3.0.0. Use the 'honeypot' service instead. For example, \Drupal::service('honeypot')->addFormProtection(\$form, \$form_state, \$options). See https://www.drupal.org/node/2949447"); + $form = []; + $form_state = new FormState(); + honeypot_add_form_protection($form, $form_state, ['honeypot']); + } + + /** + * Tests the deprecation message for honeypot_get_time_limit(). + */ + public function testGetTimeLimit(): void { + $this->expectDeprecation("honeypot_get_time_limit() is deprecated in honeypot:2.1.0 and is removed from honeypot:3.0.0. Use the 'honeypot' service instead. For example, \Drupal::service('honeypot')->getTimeLimit(\$form_values). See https://www.drupal.org/node/2949447"); + $this->assertIsInt(honeypot_get_time_limit()); + } + + /** + * Tests the deprecation message for honeypot_log_failure(). + */ + public function testLogFailure(): void { + $this->expectDeprecation("honeypot_log_failure() is deprecated in honeypot:2.1.0 and is removed from honeypot:3.0.0. Use the 'honeypot' service instead. For example, \Drupal::service('honeypot')->logFailure(\$form_id, \$type). See https://www.drupal.org/node/2949447"); + honeypot_log_failure('user_login_form', 'honeypot'); + } + +} diff --git a/web/modules/honeypot/tests/src/Unit/Integration/Event/EventTestBase.php b/web/modules/honeypot/tests/src/Unit/Integration/Event/EventTestBase.php new file mode 100644 index 0000000000000000000000000000000000000000..90f46e6f819f07e32dc979fc591712c9da914c20 --- /dev/null +++ b/web/modules/honeypot/tests/src/Unit/Integration/Event/EventTestBase.php @@ -0,0 +1,34 @@ +<?php + +namespace Drupal\Tests\honeypot\Unit\Integration\Event; + +use Drupal\rules\Core\RulesEventManager; +use Drupal\Tests\rules\Unit\Integration\Event\EventTestBase as RulesEventTestBase; + +/** + * Base class containing common code for Honeypot event tests. + * + * @group honeypot + * + * @requires module rules + */ +abstract class EventTestBase extends RulesEventTestBase { + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + // Must enable our module to make our plugins discoverable. + $this->enableModule('honeypot'); + + // Tell the plugin manager where to look for plugins. + $this->moduleHandler->getModuleDirectories() + ->willReturn(['honeypot' => __DIR__ . '/../../../../../']); + + // Create a real plugin manager with a mock moduleHandler. + $this->eventManager = new RulesEventManager($this->moduleHandler->reveal(), $this->entityTypeBundleInfo->reveal()); + } + +} diff --git a/web/modules/honeypot/tests/src/Unit/Integration/Event/HoneypotRejectEventTest.php b/web/modules/honeypot/tests/src/Unit/Integration/Event/HoneypotRejectEventTest.php new file mode 100644 index 0000000000000000000000000000000000000000..bc5c1ac3e955069cb34003d3c93e0e805b3dcb41 --- /dev/null +++ b/web/modules/honeypot/tests/src/Unit/Integration/Event/HoneypotRejectEventTest.php @@ -0,0 +1,47 @@ +<?php + +namespace Drupal\Tests\honeypot\Unit\Integration\Event; + +/** + * Tests the definition of the "honeypot.form_submission_rejected" event. + * + * @coversDefaultClass \Drupal\honeypot\Event\HoneypotRejectEvent + * + * @group honeypot + * + * @requires module rules + */ +class HoneypotRejectEventTest extends EventTestBase { + + /** + * Tests the event metadata. + */ + public function testHoneypotRejectEvent(): void { + $plugin_definition = $this->eventManager->getDefinition('honeypot.form_submission_rejected'); + $this->assertSame('After rejecting a form submission', (string) $plugin_definition['label']); + + $event = $this->eventManager->createInstance('honeypot.form_submission_rejected'); + + $form_id_context_definition = $event->getContextDefinition('form_id'); + $this->assertSame('string', $form_id_context_definition->getDataType()); + $this->assertSame( + 'Rejected form ID', + $form_id_context_definition->getLabel() + ); + + $uid_context_definition = $event->getContextDefinition('uid'); + $this->assertSame('integer', $uid_context_definition->getDataType()); + $this->assertSame( + 'Rejected user ID', + $uid_context_definition->getLabel() + ); + + $type_context_definition = $event->getContextDefinition('type'); + $this->assertSame('string', $type_context_definition->getDataType()); + $this->assertSame( + 'Reason for rejection', + $type_context_definition->getLabel() + ); + } + +}