Commit 9f22c830 authored by geerlingguy's avatar geerlingguy

Issue #1917700-1 by geerlingguy: Port Honeypot to Drupal 8.

parent afcfab68
......@@ -6,8 +6,8 @@ Honeypot Module Readme
Installation
------------
To install this module, place it in your sites/all/modules folder and enable it
on the modules page.
To install this module, place it in your modules folder and enable it on the
modules page.
Configuration
......@@ -35,4 +35,4 @@ Credit
------
The Honeypot module was originally developed by Jeff Geerling of Midwestern Mac,
LLC (midwesternmac.com), and sponsored by flockNote (flocknote.com).
\ No newline at end of file
LLC (midwesternmac.com), and sponsored by Flocknote (flocknote.com).
\ No newline at end of file
protect_all_forms: 0
log: 0
element_name: 'url'
time_limit: 5
form_settings:
user_register_form: 0
user_pass: 0
webforms: 0
feedback_contact_message_form: 0
_contact_message_form: 0
\ No newline at end of file
name = Honeypot
description = "Mitigates spam form submissions using the honeypot method."
core = 7.x
core = 8.x
configure = admin/config/content/honeypot
package = "Spam control"
files[] = honeypot.test
......@@ -47,86 +47,29 @@ function honeypot_install() {
* Implements hook_uninstall().
*/
function honeypot_uninstall() {
db_delete('variable')
->condition('name', db_like('honeypot_') . '%', 'LIKE')
->execute();
$cache_tables = array('variables', 'cache_bootstrap');
foreach ($cache_tables as $table) {
if (db_table_exists($table)) {
cache_clear_all($table, 'cache');
}
$caches = array('cache_bootstrap');
foreach ($caches as $cache) {
cache($cache)->deleteAll();
}
}
/**
* Implements hook_update_N().
*/
function honeypot_update_7001() {
$ret = array();
// Leaving this in because I had it in version 1.3. Silly me.
return $ret;
}
/**
* Update form names after upgrade from 6.x version.
* Upgrade Honeypot settings from Drupal 7 to Drupal 8.
*/
function honeypot_update_7002() {
$map = array(
'user_register' => 'user_register_form',
'contact_mail_page' => 'contact_site_form',
'contact_mail_user' => 'contact_personal_form',
);
foreach ($map as $d6_name => $d7_name) {
$value = variable_get('honeypot_form_' . $d6_name, 0);
if ($value) {
variable_set('honeypot_form_' . $d7_name, $value);
}
variable_del('honeypot_form_' . $d6_name);
}
function honeypot_update_8000() {
// If the variables table exists, there might be variables to be updated.
if (db_table_exists('variable')) {
// Get all the old variables.
$variables_to_update = db_query("SELECT name FROM {variable} WHERE name LIKE 'honeypot_%'")->fetchCol();
$comment_form_value = variable_get('honeypot_form_comment_form', 0);
if ($comment_form_value) {
$types = node_type_get_types();
if (!empty($types)) {
foreach ($types as $type) {
$d7_name = 'honeypot_form_comment_node_' . $type->type . '_form';
variable_set($d7_name, $comment_form_value);
if (!empty($variables_to_update)) {
// Build a map of old variable names to new honeypot.settings names.
$variable_map = array();
foreach ($variables_to_update as $variable) {
$variable_map[$variable] = substr($variable, 9, strlen($variable));
}
}
}
variable_del('honeypot_form_comment_form');
}
/**
* Add {honeypot_users} database table if it doesn't exist.
*/
function honeypot_update_7003() {
// Make sure the {honeypot_users} table doesn't already exist.
if (!db_table_exists('honeypot_user')) {
$table = array(
'description' => 'Table that stores failed attempts to submit a form.',
'fields' => array(
'uid' => array(
'description' => 'Foreign key to {users}.uid; uniquely identifies a Drupal user to whom this ACL data applies.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
),
'timestamp' => array(
'description' => 'Date/time when the form submission failed, as Unix timestamp.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
),
),
'indexes' => array(
'uid' => array('uid'),
'timestamp' => array('timestamp'),
),
);
db_create_table('honeypot_user', $table);
update_variables_to_config('honeypot.settings', $variable_map);
}
}
}
......@@ -2,7 +2,6 @@
/**
* @file
*
* Honeypot module, for deterring spam bots from completing Drupal forms.
*/
......@@ -13,10 +12,10 @@ function honeypot_menu() {
$items['admin/config/content/honeypot'] = array(
'title' => 'Honeypot configuration',
'description' => 'Configure Honeypot spam prevention and the forms on which Honeypot will be used.',
'page callback' => 'drupal_get_form',
'page arguments' => array('honeypot_admin_form'),
// TODO - Remove once http://drupal.org/node/1845402 is resolved?
'page callback' => 'NOT_USED',
'access callback' => 'user_access',
'access arguments' => array('administer honeypot'),
'file' => 'honeypot.admin.inc',
);
return $items;
......@@ -43,8 +42,9 @@ function honeypot_permission() {
*/
function honeypot_cron() {
// Delete {honeypot_user} entries older than the value of honeypot_expire.
$expire_limit = config('honeypot.settings')->get('expire');
db_delete('honeypot_user')
->condition('timestamp', time() - variable_get('honeypot_expire', 300), '<')
->condition('timestamp', time() - $expire_limit, '<')
->execute();
}
......@@ -52,6 +52,9 @@ function honeypot_cron() {
* Implements hook_form_alter().
*
* Add Honeypot features to forms enabled in the Honeypot admin interface.
*
* @todo - Check all forms and definitions to make sure nothing's changed in the
* D7 to D8 upgrade cycle.
*/
function honeypot_form_alter(&$form, &$form_state, $form_id) {
// Don't use for maintenance mode forms (install, update, etc.).
......@@ -67,7 +70,8 @@ function honeypot_form_alter(&$form, &$form_state, $form_id) {
);
// If configured to protect all forms, add protection to every form.
if (variable_get('honeypot_protect_all_forms', 0) && !in_array($form_id, $unprotected_forms)) {
$protect_all_forms = config('honeypot.settings')->get('protect_all_forms');
if ($protect_all_forms && !in_array($form_id, $unprotected_forms)) {
// Don't protect system forms - only admins should have access, and system
// forms may be programmatically submitted by drush and other modules.
if (strpos($form_id, 'system_') === FALSE && strpos($form_id, 'search_') === FALSE && strpos($form_id, 'views_exposed_form_') === FALSE) {
......@@ -92,29 +96,31 @@ function honeypot_form_alter(&$form, &$form_state, $form_id) {
/**
* Build an array of all the protected forms on the site, by form_id.
*
* @todo - Add in API call/hook to allow modules to add to this array.
*/
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 = cache_get('honeypot_protected_forms')) {
if ($cache = cache()->get('honeypot_protected_forms')) {
$forms = $cache->data;
}
else {
// Look up all the honeypot forms in the variables table.
$result = db_query("SELECT name FROM {variable} WHERE name LIKE 'honeypot_form_%'")->fetchCol();
// Add each form that's enabled to the $forms array.
foreach ($result as $variable) {
if (variable_get($variable, 0)) {
$forms[] = substr($variable, 14);
$form_settings = 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 = array();
}
// Save the cached data.
cache_set('honeypot_protected_forms', $forms, 'cache');
cache()->set('honeypot_protected_forms', $forms);
}
}
......@@ -178,6 +184,8 @@ function honeypot_add_form_protection(&$form, &$form_state, $options = array())
// Disable page caching to make sure timestamp isn't cached.
if (user_is_anonymous()) {
// TODO D8 - Use DIC? See: http://drupal.org/node/1539454
// Should this now set 'omit_vary_cookie' instead?
$GLOBALS['conf']['cache'] = 0;
}
}
......@@ -294,6 +302,6 @@ function honeypot_log_failure() {
}
// Register flood event for anonymous users.
else {
flood_register_event('honeypot');
drupal_container()->get('flood')->register('honeypot.event');
}
}
honeypot__config:
pattern: '/admin/config/content/honeypot'
defaults:
_controller: 'honeypot.settings.form:getForm'
requirements:
_permission: 'administer honeypot'
\ No newline at end of file
<?php
/**
* @file
* Contains Drupal\honeypot\HoneypotBundle.
*/
namespace Drupal\honeypot;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
/**
* Provides the honeypot dependency injection container.
*/
class HoneypotBundle extends Bundle {
/**
* Overrides \Symfony\Component\HttpKernel\Bundle\Bundle::build().
*/
public function build(ContainerBuilder $container) {
// Register the HoneypotSettingsController class with the DIC.
$container->register('honeypot.settings.form', 'Drupal\honeypot\HoneypotSettingsController');
}
}
\ No newline at end of file
<?php
/**
* @file
* Contains Drupal\honeypot\HoneypotSettingsController.
*/
namespace Drupal\honeypot;
use Drupal\Core\Form\FormInterface;
/**
* Returns responses for Honeypot module routes.
*/
class HoneypotSettingsController implements FormInterface {
/**
* Creates a new instance of this form.
*/
public function getForm() {
return drupal_get_form($this);
}
/**
* Get a value from the retrieved form settings array.
*/
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;
}
}
/**
* Implements \Drupal\Core\Form\FormInterface::getFormID().
*/
public function getFormID() {
return 'honeypot_settings_form';
}
/**
* Implements \Drupal\Core\Form\FormInterface::buildForm().
*/
public function buildForm(array $form, array &$form_state) {
// Honeypot Configuration.
$form['configuration'] = array(
'#type' => 'fieldset',
'#title' => t('Honeypot Configuration'),
'#collapsible' => TRUE,
'#collapsed' => FALSE,
);
$form['configuration']['protect_all_forms'] = array(
'#type' => 'checkbox',
'#title' => t('Protect all forms with Honeypot'),
'#description' => t('This will enable Honeypot protection for ALL forms on this site, regardless of the settings in the Honeypot enabled forms section below.'),
'#default_value' => config('honeypot.settings')->get('protect_all_forms'),
);
$form['configuration']['log'] = array(
'#type' => 'checkbox',
'#title' => t('Log blocked form submissions'),
'#description' => t('Log submissions that are blocked due to Honeypot protection.'),
'#default_value' => config('honeypot.settings')->get('log'),
);
$form['configuration']['element_name'] = array(
'#type' => 'textfield',
'#title' => t('Honeypot element name'),
'#description' => t("The name of the Honeypot form field. It's usually most effective to use a generic name like email, homepage, or name, 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' => config('honeypot.settings')->get('element_name'),
'#required' => TRUE,
'#size' => 30,
);
$form['configuration']['time_limit'] = array(
'#type' => 'textfield',
'#title' => t('Honeypot time limit'),
'#description' => t('Minimum time required before form should be considered entered by a human instead of a bot. Set to 0 to disable. <strong>Page caching will be disabled if there is a form protected by time limit on the page.</strong>'),
'#default_value' => config('honeypot.settings')->get('time_limit'),
'#required' => TRUE,
'#size' => 5,
'#field_suffix' => t('seconds'),
);
// Honeypot Enabled forms.
$form_settings = config('honeypot.settings')->get('form_settings');
$form['form_settings'] = array(
'#type' => 'fieldset',
'#title' => t('Honeypot Enabled Forms'),
'#description' => t("Check the boxes next to individual forms on which you'd like Honeypot protection enabled."),
'#collapsible' => TRUE,
'#collapsed' => FALSE,
'#tree' => TRUE,
'#states' => array(
// Hide this fieldset when all forms are protected.
'invisible' => array(
'input[name="protect_all_forms"]' => array('checked' => TRUE),
),
),
);
// Generic forms.
$form['form_settings']['general_forms'] = array('#markup' => '<h5>' . t('General Forms') . '</h5>');
// User register form.
$form['form_settings']['user_register_form'] = array(
'#type' => 'checkbox',
'#title' => t('User Registration form'),
'#default_value' => $this->getFormSettingsValue($form_settings, 'user_register_form'),
);
// User password form.
$form['form_settings']['user_pass'] = array(
'#type' => 'checkbox',
'#title' => t('User Password Reset form'),
'#default_value' => $this->getFormSettingsValue($form_settings, 'user_pass'),
);
// If webform.module enabled, add webforms.
// TODO D8 - See if D8 version of Webform.module still uses this form ID.
if (module_exists('webform')) {
$form['form_settings']['webforms'] = array(
'#type' => 'checkbox',
'#title' => t('Webforms (all)'),
'#default_value' => $this->getFormSettingsValue($form_settings, 'webforms'),
);
}
// If contact.module enabled, add contact forms.
if (module_exists('contact')) {
// TODO D8 - Sitewide contact forms are now dynamically-named.
$form['form_settings']['contact_forms'] = array('#markup' => '<h5>' . t('Contact Forms') . '</h5>');
// Sitewide contact form.
$form['form_settings']['contact_site_form'] = array(
'#type' => 'checkbox',
'#title' => t('Sitewide Contact form'),
'#default_value' => $this->getFormSettingsValue($form_settings, 'contact_site_form'),
);
// Sitewide personal form.
$form['form_settings']['contact_personal_form'] = array(
'#type' => 'checkbox',
'#title' => t('Personal Contact forms'),
'#default_value' => $this->getFormSettingsValue($form_settings, '_contact_message_form'),
);
}
// Get node types for node forms and node comment forms.
$types = node_type_get_types();
if (!empty($types)) {
// Node forms.
$form['form_settings']['node_forms'] = array('#markup' => '<h5>' . t('Node Forms') . '</h5>');
foreach ($types as $type) {
$id = $type->type . '_node_form';
$form['form_settings'][$id] = array(
'#type' => 'checkbox',
'#title' => t('@name node form', array('@name' => $type->name)),
'#default_value' => $this->getFormSettingsValue($form_settings, $id),
);
}
// Comment forms.
if (module_exists('comment')) {
$form['form_settings']['comment_forms'] = array('#markup' => '<h5>' . t('Comment Forms') . '</h5>');
foreach ($types as $type) {
$id = 'comment_node_' . $type->type . '_comment_form';
$form['form_settings'][$id] = array(
'#type' => 'checkbox',
'#title' => t('@name comment form', array('@name' => $type->name)),
'#default_value' => $this->getFormSettingsValue($form_settings, $id),
);
}
}
}
// 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'] = array(
'#type' => 'submit',
'#value' => t('Save configuration'),
);
return $form;
}
/**
* Implements \Drupal\Core\Form\FormInterface::validateForm().
*/
public function validateForm(array &$form, array &$form_state) {
// Make sure the time limit is a positive integer or 0.
$time_limit = $form_state['values']['time_limit'];
if ((is_numeric($time_limit) && $time_limit > 0) || $time_limit === '0') {
if (ctype_digit($time_limit)) {
// Good to go.
}
else {
form_set_error('time_limit', t("The time limit must be a positive integer or 0."));
}
}
else {
form_set_error('time_limit', 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['values']['element_name'])) {
form_set_error('element_name', t("The element name cannot contain spaces or other special characters."));
}
}
/**
* Implements \Drupal\Core\Form\FormInterface::submitForm().
*/
public function submitForm(array &$form, array &$form_state) {
$config = config('honeypot.settings');
// Save all the non-form-id values from $form_state.
foreach ($form_state['values'] as $key => $value) {
if ($key != 'form_settings') {
$config->set($key, $value);
}
}
// Save the honeypot forms from $form_state into a 'form_settings' array.
$config->set('form_settings', $form_state['values']['form_settings']);
$config->save();
// Clear the honeypot protected forms cache.
cache_invalidate_tags(array('honeypot_protected_forms' => TRUE));
}
}
\ No newline at end of file
<?php
/**
* @file
* Definition of Drupal\honeypot\Tests\HoneypotFormTestCase.
*/
namespace Drupal\example\Tests;
use Drupal\Core\Database\Database;
/**
* Test the functionality of the Honeypot module for an admin user.
*/
class HoneypotFormTestCase extends DrupalWebTestCase {
protected $admin_user;
protected $web_user;
protected $node;
public static function getInfo() {
return array(
'name' => 'Honeypot form protections',
'description' => 'Ensure that Honeypot protects site forms properly.',
'group' => 'Form API',
);
}
public function setUp() {
// Enable modules required for this test.
parent::setUp(array('honeypot', 'comment'));
// Set up required Honeypot variables.
variable_set('honeypot_element_name', 'url');
variable_set('honeypot_time_limit', 0); // Disable time_limit protection.
variable_set('honeypot_protect_all_forms', TRUE); // Test protecting all forms.
variable_set('honeypot_log', FALSE);
// Set up other required variables.
variable_set('user_email_verification', TRUE);
variable_set('user_register', USER_REGISTER_VISITORS);
// Set up admin user.
$this->admin_user = $this->drupalCreateUser(array(
'administer honeypot',
'bypass honeypot protection',
'administer content types',
'administer users',
'access comments',
'post comments',
'skip comment approval',
'administer comments',
));
// Set up web user.
$this->web_user = $this->drupalCreateUser(array(
'access comments',
'post comments',
'create article content',
));
// Set up example node.
$this->node = $this->drupalCreateNode(array(
'type' => 'article',
'promote' => 1,
'uid' => $this->web_user->uid,
));
}
/**
* Test user registration (anonymous users).
*/
public function testProtectRegisterUserNormal() {
// Set up form and submit it.
$edit['name'] = $this->randomName();
$edit['mail'] = $edit['name'] . '@example.com';
$this->drupalPost('user/register', $edit, t('Create new account'));
// Form should have been submitted successfully.
$this->assertText(t('A welcome message with further instructions has been sent to your e-mail address.'), 'User registered successfully.');
}
public function testProtectUserRegisterHoneypotFilled() {
// Set up form and submit it.
$edit['name'] = $this->randomName();
$edit['mail'] = $edit['name'] . '@example.com';
$edit['url'] = 'http://www.example.com/';
$this->drupalPost('user/register', $edit, t('Create new account'));
// Form should have error message.
$this->assertText(t('There was a problem with your form submission. Please refresh the page and try again.'), 'Registration form protected by honeypot.');
}
public function testProtectRegisterUserTooFast() {
// Enable time limit for honeypot.
variable_set('honeypot_time_limit', 5);
// Set up form and submit it.
$edit['name'] = $this->randomName();
$edit['mail'] = $edit['name'] . '@example.com';
$this->drupalPost('user/register', $edit, t('Create new account'));
// Form should have error message.
$this->assertText(t('There was a problem with your form submission. Please wait'), 'Registration form protected by time limit.');
}
public function testProtectCommentFormNormal() {
$comment = 'Test comment.';
// Disable time limit for honeypot.
variable_set('honeypot_time_limit', 0);
// Log in the web user.
$this->drupalLogin($this->web_user);
// Set up form and submit it.
$edit['comment_body[' . LANGUAGE_NONE . '][0][value]'] = $comment;
$this->drupalPost('comment/reply/' . $this->node->nid, $edit, t('Save'));
$this->assertText(t('Your comment has been posted.'), 'Comment posted successfully.');
}
public function testProtectCommentFormHoneypotFilled() {
$comment = 'Test comment.';
// Log in the web user.
$this->drupalLogin($this->web_user);
// Set up form and submit it.
$edit['comment_body[' . LANGUAGE_NONE . '][0][value]'] = $comment;
$edit['url'] = 'http://www.example.com/';
$this->drupalPost('comment/reply/' . $this->node->nid, $edit, t('Save'));
$this->assertText(t('There was a problem with your form submission. Please refresh the page and try again.'), 'Comment posted successfully.');
}
public function testProtectCommentFormHoneypotBypass() {
// Log in the admin user.
$this->drupalLogin($this->admin_user);
// Get the comment reply form and ensure there's no 'url' field.
$this->drupalGet('comment/reply/' . $this->node->nid);
$this->assertNoText('id="edit-url" name="url"', 'Honeypot home page field not shown.');
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment