Making a contributing module for fighting spam

Site has moved

This site has moved to a new location. Visit the new site at http://programsdream.nl.

By Ronald van Belzen | May 9, 2018

Making a new module starts with an idea for the module. In this case it was trying to make a module that can replace Mollow to some extend (see previous blog post).

Finding a name for your module can be a challenge, but whatever name you pick, be sure that the machine name of your module is available. Try whether the project exist by visiting https://www.drupal.org/project/{my_module_name}. A page not found (404) response is a good enough indicator to confirm that your module name is still available.

Next step is to read the documentation. The best starting point seems to be Contribute to development. To gather all the information you need to follow half a dozen links, but as far as I can tell all the information is there.

I had a look at the Mods & Plugins that make use of the service provided by Stop Forum Spam, and the concensus seems to be to name the mod after the service it makes use of. So I went for the name Stop Forum Spam Client. Don't go there before I finish this series of articles. I will release a fully functional and tested version soon after that.

# sfs.info.yml
name: 'Stop Forum Spam Client'
type: module
description: 'Client that makes use of the www.StopForumSpam.com api services for blocking spam, spammers and spambots.'
configure: sfs.settings_form
package: 'Spam control'
version: '1.0'
core: '8.x'

As you can see in the above info file the module has no dependencies, while the module may depend on the presence of some modules, but must be able to be installed in their absence. The configuration setting form will imediately show a way to solve this dilemma. And what these modules are will become clear when looking at the configuration install parameters.

# /config/install/sfs.settings.yml
sfs_api_key: ''
sfs_check_user_registration: TRUE
sfs_check_node: FALSE
sfs_check_comment: FALSE
sfs_check_contact: FALSE
sfs_criteria_email: 5
sfs_criteria_username: 0
sfs_criteria_ip: 20
sfs_cron_job: FALSE
sfs_cron_blocked_accounts: FALSE
sfs_cron_account_limit: 0
sfs_cron_account_action: 0
sfs_cron_last_uid: 1
sfs_flood_delay: 0
sfs_log_blocked_spam: FALSE
sfs_whitelist_emails: ''
sfs_whitelist_usernames: ''
sfs_whitelist_ips: ''
sfs_blocked_message_email: 'Your email address is blacklisted.'
sfs_blocked_message_username: 'Your username is blacklisted.'
sfs_blocked_message_ip: 'Your IP address is blacklisted.'

The ambition is to check comments and contact form submissions too, but these modules are not enabled by every site. So, lets not set the values for sfs_check_comment and sfs_check_contact, not even allow them to be set when the corresponding modules are not enabled.

For this the ModuleHandler is made available by dependency injection and used in the validateForm() method to prevent the settings of these values when the corresponding modules are not enabled. Apart from that the SfsConfigForm class is straightforward.

If you are new to writing configuration forms, you can find more information here, and more about dependency injection here.

<?php
// /src/Form/SfsConfigForm.php

namespace Drupal\sfs\Form;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Symfony\Component\DependencyInjection\ContainerInterface;

class SfsConfigForm extends ConfigFormBase {
  
  /**
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;
  
  
  public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler) {
    parent::__construct($config_factory);
    
    $this->moduleHandler = $module_handler;
  }
  
  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('config.factory'),
      $container->get('module_handler')
    );
  }
  
  /**
   * {@inheritdoc}
   */
  protected function getEditableConfigNames() {
    return ['sfs.settings'];
  }
  
  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'sfs_settings_form';
  }
  
  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $config = $this->config('sfs.settings');
    
    // Fieldset for check
    $form['check'] = [
      '#type' => 'details',
      '#title' => $this->t('Check activities'),
      '#description' => $this->t('You can include the following activities to be blocked for spammers.'),
      '#collapsible' => TRUE,
    ];
    $form['check']['sfs_check_user_registration'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Check user registration'),
      '#default_value' => $config->get('sfs_check_user_registration'),
    ];
    $form['check']['sfs_check_node'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Check posting content'),
      '#default_value' => $config->get('sfs_check_node'),
    ];
    $form['check']['sfs_check_comment'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Check posting comments'),
      '#default_value' => $config->get('sfs_check_comment'),
    ];
    $form['check']['sfs_check_contact'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Check contact form submissions'),
      '#default_value' => $config->get('sfs_check_contact'),
    ];
    // Fieldset for flood
    $form['flood'] = [
      '#type' => 'details',
      '#title' => $this->t('Flood protection'),
      '#collapsible' => TRUE,
    ];
    $form['flood']['sfs_flood_delay'] = [
      '#type' => 'select',
      '#title' => $this->t('Delay spammer for'),
      '#description' => $this->t('If an user is blacklisted you can delay the request. This can be useful when spammers flood your site.'),
      '#options' => $this->getFloodOptions(),
      '#default_value' => $config->get('sfs_flood_delay'),
    ];
    // Fieldset for criteria
    $form['criteria'] = [
      '#type' => 'details',
      '#title' => $this->t('Spam block criteria'),
      '#description' => $this->t('A user activity will be considered to be spam when the email, username, or IP address has been reported to www.stopforumspam.com more times than the following thresholds.'),
      '#collapsible' => TRUE,
    ];
    $form['criteria']['sfs_criteria_email'] = [
      '#type' => 'select',
      '#title' => $this->t('Number of times the email has been reported is equal to or more than'),
      '#description' => $this->t('If the email address for a user has been reported to www.stopforumspam.com this many times, then the user will be considered to be a spammer.'),
      '#options' => $this->getCriteriaOptions($this->t("Don't use email as a criterium")), 
      '#default_value' => $config->get('sfs_criteria_email'),
    ];
    $form['criteria']['sfs_criteria_username'] = [
      '#type' => 'select',
      '#title' => $this->t('Number of times the username has been reported is equal to or more than'),
      '#description' => $this->t('If the username for a user has been reported to www.stopforumspam.com this many times, then the user will be considered to be a spammer. CAUTION: This criterium may result in false positives.'),
      '#options' => $this->getCriteriaOptions($this->t("Don't use username as a criterium")),
      '#default_value' => $config->get('sfs_criteria_username'),
    ];
    $form['criteria']['sfs_criteria_ip'] = [
      '#type' => 'select',
      '#title' => $this->t('Number of times the IP address has been reported is equal to or more than'),
      '#description' => $this->t('If the IP address for a user or user registration has been reported to www.stopforumspam.com this many times, then the user will be considered to be a spammer.'),
      '#options' => $this->getCriteriaOptions($this->t("Don't use IP address as a criterium")),
      '#default_value' => $config->get('sfs_criteria_ip'),
    ];
    // Fieldset for white list
    $form['whitelist'] = [
      '#type' => 'details',
      '#title' => $this->t('Whitelists'),
      '#collapsible' => TRUE,
    ];
    $form['whitelist']['sfs_whitelist_emails'] = [
      '#type' => 'textarea',
      '#title' => $this->t('Allowed email addresses'),
      '#description' => $this->t('Enter email addresses (one per line).'),
      '#default_value' => $config->get('sfs_whitelist_emails'),
    ];
    $form['whitelist']['sfs_whitelist_usernames'] = [
      '#type' => 'textarea',
      '#title' => $this->t('Allowed usernames'),
      '#description' => $this->t('Enter usernames (one per line).'),
      '#default_value' => $config->get('sfs_whitelist_usernames'),
    ];
    $form['whitelist']['sfs_whitelist_ips'] = [
      '#type' => 'textarea',
      '#title' => $this->t('Allowed IP addresses'),
      '#description' => $this->t('Enter IP addresses (one per line).'),
      '#default_value' => $config->get('sfs_whitelist_ips'),
    ];
    // Fieldset for logging
    $form['logging'] = [
      '#type' => 'details',
      '#title' => $this->t('Logging'),
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
    ];
    $form['logging']['sfs_log_blocked_spam'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Log information about blocked activity into Drupal log'),
      '#default_value' => $config->get('sfs_log_blocked_spam'),
    ];
    
    $form['sfs_api_key'] = [
      '#type' => 'textfield',
      '#title' => $this->t('www.stopforumspam.com API Key'),
      '#description' => $this->t('For reporting spammers to StopForumSpam you will need to register for an API Key at the <a href="http://www.stopforumspam.com">StopForumSpam</a> website.'),
      '#default_value' => $config->get('sfs_api_key'),
    ];
    
    return parent::buildForm($form, $form_state);
    
  }
  
  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $config = $this->config('sfs.settings');
    $config
    ->set('sfs_check_user_registration', $form_state->getValue('sfs_check_user_registration'))
    ->set('sfs_check_node', $form_state->getValue('sfs_check_node'))
    ->set('sfs_check_comment', $form_state->getValue('sfs_check_comment'))
    ->set('sfs_check_contact', $form_state->getValue('sfs_check_contact'))
    ->set('sfs_flood_delay', $form_state->getValue('sfs_flood_delay'))
    ->set('sfs_criteria_email', $form_state->getValue('sfs_criteria_email'))
    ->set('sfs_criteria_username', $form_state->getValue('sfs_criteria_username'))
    ->set('sfs_criteria_ip', $form_state->getValue('sfs_criteria_ip'))
    ->set('sfs_whitelist_emails', $form_state->getValue('sfs_whitelist_emails'))
    ->set('sfs_whitelist_usernames', $form_state->getValue('sfs_whitelist_usernames'))
    ->set('sfs_whitelist_ips', $form_state->getValue('sfs_whitelist_ips'))
    ->set('sfs_log_blocked_spam', $form_state->getValue('sfs_log_blocked_spam'))
    ->set('sfs_api_key', $form_state->getValue('sfs_api_key'))
    ->save();
    
    parent::submitForm($form, $form_state);
  }
  
  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
    parent::validateForm($form, $form_state);
    
    if ($form_state->getValue('sfs_check_comment') && !$this->moduleHandler->moduleExists('comment')) {
      $message = $this->t('Please enable the comment module before enabling the spam checking of comments.');
      $form_state->setError($form['sfs_check_comment'], $message);
    }
    if ($form_state->getValue('sfs_check_contact') && !$this->moduleHandler->moduleExists('contact')) {
      $message = $this->t('Please enable the contact module before enabling the spam checking of contact feedback.');
      $form_state->setError($form['sfs_check_contact'], $message);
    }
  }
  
  /**
   * Options list for spam block criteria.
   * 
   * @param TranslatableMarkup $label
   * @return array
   */
  protected function getCriteriaOptions(TranslatableMarkup $label) {
    return [
      0 => $label,
      1 => 1,
      2 => 2,
      3 => 3,
      4 => 4,
      5 => 5,
      6 => 6,
      7 => 7,
      8 => 8,
      9 => 9,
      10 => 10,
      15 => 15,
      20 => 20,
      30 => 30,
      40 => 40,
      50 => 50,
      60 => 60,
      70 => 70,
      80 => 80,
      90 => 90,
      100 => 100,
      999 => $this->t('Blacklisted by StopForumSpam'),
    ];
  }
  
  /**
   * Options list for flood protection delay.
   * 
   * @return array
   */
  protected function getFloodOptions() {
    $options = [0 => $this->t("Do not delay"), 1 => $this->t('1 second')];
    foreach ([2, 3, 4, 5, 10, 15, 20, 30] as $second) {
      $options[$second] = $this->t('@number seconds', ['@number' => $second]);
    }
    return $options;
  }
}

Of course, the routing to this form need to be defined.

# sfs.routing.yml
sfs.settings_form:
  path: '/admin/config/system/sfs'
  defaults:
    _title: 'Stop Forum Spam Client'
    _form: '\Drupal\sfs\Form\SfsConfigForm'
  requirements:
    _permission: 'administer site configuration'

And the link to the configuration form needs to be made available.

# sfs.links.menu.yml
sfs.settings_form:
  route_name: sfs.settings_form
  title: Stop Forum Spam Client
  description: 'Configure the Stop Forum Spam Client module'
  parent: system.admin_config_system

Now we got that out of the way we can start writing a class that retrieves the information from the Stop Forum Spam API. But, since I have not written that functionality yet, that will be part of the next part in this series.

Comments

Ronald van Belzen wrote on Sat, 06/16/2018 - 10:24

Maybe this will interest some people.

This site uses Honeypot to intercept spambots. This intercepts more than 90% of all spam attempts, because most spammers are spambots.

As a second line of defense this sites uses the Stop Forum Spam Client. This module works fine against known-spammers, but not all spammers are known at stopforumspam.com. The module works best to prevent known spammers from registering at this site.

This site allows anonymous visitors to place comments. This could be managed by comment approval, but it will require a lot of work when the site becomes popular amongst spammers. To alleviate the amount of work involved in comment approval (or even abolish it) I use Statistical Spam Filter.