Reporting spammers to SFS

Site has moved

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

By Ronald van Belzen | May 17, 2018

The next step is reporting spam to stopforumspam.com. This will be an action initiated by a maintainer who has spotted spam, preferably by one click on a button. This action will need to be handled by the software by sending the report to stopforumspam.com. We we look at the latter first and adding buttons to start the report after that.

As an example we concentrate on comments. The function that needs to be called (commentReport()) first checks whether there is a token (or api key) defined. It also checks whether the user is anonymous. You cannot report an anonymous comment, since stopforumspam.com requires you to fill in name, e-mail address and ip address to report spam and for an anonymous comment post you only have the ip address.

/* /src/ReportSpam */

  /**
   * Report for a comment.
   * 
   * @param int $comment_id
   */
  public function commentReport(int $comment_id = 0) {
    $token = $this->config->get('sfs_api_key');
    $comment = Comment::load($comment_id);
    $user = $comment->getOwner();
    if(empty($token)) {
      $this->messenger->addWarning($this->t('No api key found for reporting to stopforumspam.com. Cannot report without a valid key.'));
    }
    elseif ($user->isAnonymous()) {
      $this->messenger->addWarning($this->t('Anonymous users should not be reported to stopforumspam.com. Comment is not reported.'));
    }
    else {
      $name = $user->getUsername();
      $mail = $user->getEmail();
      $subject = $comment->getSubject();
      $body = $comment->get('comment_body')->value;
      $ip = $comment->getHostname();
      
      if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === FALSE) {
        $message = $this->t('Invalid IP address on comment: @ip. SFS Client will not report invalid ip addresses or ip addresses within private or reserved ip ranges.', ['@ip' => $ip]);
        $this->messenger->addStatus($message);
        $this->log->notice($message);
      }
      else {
        $evidence = '<p>' . $subject . '</p>' . $body;
        if ($this->reportSpam($token, $name, $mail, $ip, $evidence)) {
          $this->messenger->addStatus($this->t('The comment @id was reported successfully to stopforumspam.com.', ['@id' => $comment_id]));
          try {
            $comment->setUnpublished()->save();
            $this->log->notice($this->t('The comment @id was unpublished.', ['@id' => $comment_id]));
          }
          catch (EntityStorageException $e) {
            $this->messenger->addError($this->t('Failed to unpublish comment @id: @error', ['@id' => $comment_id, '@error' => $e->getMessage()]));
            $this->log->error('Failed to unpublish comment @id: @error', ['@id' => $comment_id, '@error' => $e->getMessage()]);
          }
        }
      }
    }
  }

$this->reportSpam() does the actual sending of the report, which is, just like requesting data, a simple function.

/* /src/ReportSpam.php */
  /**
   * Report spammer to www.stopforumspam.com.
   * 
   * @param string $token
   * @param string $name
   * @param string $mail
   * @param string $ip_address
   * @param string $evidence
   * @return boolean
   */
  protected function reportSpam($token, $name, $mail, $ip_address, $evidence) {
    $name = urlencode($name);
    $mail = urlencode($mail);
    $evidence = urlencode($evidence);
    $data = "username={$name}&ip_addr={$ip_address}&evidence={$evidence}&email={$mail}&api_key={$token}";
    $options = [
      'headers' => [
        'Content-type' => 'application/x-www-form-urlencoded',
        'Content-length' => strlen($data),
        'Connection' => 'close',
      ],
      'body' => $data,
    ];
    try {
      $response = $this->httpClient->request('POST', 'https://www.stopforumspam.com/add.php', $options);
      $data = (string) $response->getBody();
    }
    catch (RequestException $e) {
      $this->messenger->addError($this->t('Failed to report spam due to "%error".', ['%error' => $e->getMessage()]));
      return FALSE;
    }
    
    return TRUE;
  }
}

To trigger a report we will first add a tab to the comment Edit page, that already shows the 'View', 'Edit' and 'Delete' tabs. We do this with a links task definition.

# sfs.links.task.yml

sfs.comment.report_form_tab:
  route_name: sfs.comment.report_form
  title: Report Spam
  base_route: entity.comment.canonical
  weight: 20

The base route is the machine name of the route to the page on which to display the tab. The title is shown as the label for the tab.The route name is the route name of the form to display when the tab is selected. The route definition for this route is as follows.

# sfs.routing.yml

sfs.comment.report_form:
  path: '/comment/{comment}/report'
  defaults:
    _form: '\Drupal\sfs\Form\ConfirmReportCommentForm'
    _title: 'Report Spam'
  requirements:
    _permission: 'report sfs comment'

The form is a confirm form that will give the user the option to continue (report the comment as spam) or cancel the action.

<?php
/* /src/Form/ConfirmReportCommentForm.php */

namespace Drupal\sfs\Form;

use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\sfs\ReportSpam;

class ConfirmReportCommentForm extends ConfirmFormBase {
  protected $to_report_id;
  protected $reportSpam;
  
  /**
   * Class constructor.
   */
  public function __construct(ReportSpam $report_spam) {
    $this->reportSpam = $report_spam;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {

    return new static(
      $container->get('sfs.report.spam')
    );
  }
  
  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'sfs_confirm_comment_report';
  }
  
  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state, $comment = '') {
    $this->to_report_id = (int) $comment;
    
    $form = parent::buildForm($form, $form_state);
    return $form;
  }
  
  /**
   * {@inheritdoc}
   */
  public function getQuestion() {
    return $this->t('Are you sure you want to report comment %id as spam?', ['%id' => $this->to_report_id]);
  }
  
  /**
   * {@inheritdoc}
   */
  public function getCancelUrl() {
    return new Url('entity.comment.edit_form', ['comment' => $this->to_report_id]);
  }
  
  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $this->reportSpam->commentReport($this->to_report_id);
    
    return new Url('entity.comment.edit_form', ['comment' => $this->to_report_id]);
  }
}

The submitForm() of this Form calls the commentReport() function of the ReportSpam class.

I also want to reach this confirm form from the display of the list of comments. For this we need to add an operation to the existing operations for the entries in the list. We do this with a hook that in this case does the same thing for users and nodes lists.

/* sfs.module */

/**
 * Adds Spam Report operation to the administration lists of content, comment
 * and user.
 * 
 * Implements hook_entity_operation_alter().
 */
function sfs_entity_operation_alter(array &$operations, EntityInterface $entity) {
  if (!(\Drupal::currentUser()->hasPermission('administer sfs'))) {
    return;
  }
  
  $entityTypeId = $entity->getEntityTypeId();
  if ($entityTypeId === 'comment') {
    $operations['report_comment'] = [
      'title' => 'Report Spam',
      'url' => Url::fromRoute('sfs.comment.report_form', ['comment' => $entity->id()]),
      'weight' => 50,
    ];
  }
  if ($entityTypeId === 'node') {
    $operations['report_node'] = [
      'title' => 'Report Spam',
      'url' => Url::fromRoute('sfs.node.report_form', ['node' => $entity->id()]),
      'weight' => 50,
    ];
  }
  if ($entityTypeId === 'user') {
    $operations['report_user'] = [
      'title' => 'Report Spammer',
      'url' => Url::fromRoute('sfs.user.report_form', ['user' => $entity->id()]),
      'weight' => 50,
    ];
  }
}

Comments are special in that they are diplayed on a content page as comments belonging to that content. For each comment a 'Reply' link can be shown, and for maintainers also 'Edit' and 'Delete' links (that can be styled as buttons). To these we want to add a 'Report Spam' link for that comment. To accomplish this we need an entirely different kind of hook: the theme preprocessor. The preprocessors are a common part of theme definitions, but they can also be defined as part of a module.

/* sfs.module */

/**
 * Add Report Spam link to the links of the display of comment.
 * 
 * Implements hook_preprocess_links__comment().
 */
function sfs_preprocess_links__comment(&$variables) {
  $checkCommentSpam = \Drupal::config('sfs.settings')->get('sfs_check_comment');
  $user = \Drupal::currentUser();
  $reportCommentPermission = $user->hasPermission('report sfs comment');
  $adminCommentPermission = $user->hasPermission('administer comments');
  
  if ($checkCommentSpam && $adminCommentPermission && $reportCommentPermission) {
    $url = $variables['links']['comment-edit']['link']['#url'];
    $routeParameters = $url->getRouteParameters();
    $variables['links']['comment_report'] = [
      'link' => [
        '#type' => 'link',
        '#title' => t('Report Spam'),
        '#options' => ['ajax' => NULL],
        '#url' => new Url('sfs.comment.report_form', ['comment' => $routeParameters['comment']]),
        '#ajax' => NULL,
      ],
      'text' => t('Report Spam'),
    ];
  }
}

A little trick is used by first determining the comment id by analysing one of the already existing links of a comment, and use this id to tell the link with what id it needs to create the url for the link. The route parameter that represents the id of a comment is called 'comment'.

This covers all the type of actions the allows us the initiated the report of comments.

What is left is to create a cronjob that will scan registered users to find out whether any of them are known spammers. This will be the subject of the next blog.