You are here

RateBotDetector.php in Rate 8

Namespace

Drupal\rate

File

src/RateBotDetector.php
View source
<?php

namespace Drupal\rate;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Messenger\MessengerTrait;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Symfony\Component\HttpFoundation\RequestStack;

/**
 * The rate.bot_detector service.
 */
class RateBotDetector {
  use StringTranslationTrait;
  use MessengerTrait;

  /**
   * Client IP.
   *
   * @var string
   */
  protected $ip;

  /**
   * HTTP User agent.
   *
   * @var string
   */
  protected $agent;

  /**
   * The config factory wrapper to fetch settings.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $config;

  /**
   * Database connection object.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $database;

  /**
   * The Http Client object.
   *
   * @var \GuzzleHttp\Client
   */
  protected $httpClient;

  /**
   * The request stack.
   *
   * @var \Symfony\Component\HttpFoundation\RequestStack
   */
  protected $requestStack;

  /**
   * RateBotDetector constructor.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The configuration factory.
   * @param \Drupal\Core\Database\Connection $database
   *   Database connection object.
   * @param \GuzzleHttp\Client $http_client
   *   Http client object.
   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
   *   Database connection object.
   */
  public function __construct(ConfigFactoryInterface $config_factory, Connection $database, Client $http_client, RequestStack $request_stack) {
    $this->config = $config_factory
      ->get('rate.settings');
    $this->database = $database;
    $this->httpClient = $http_client;
    $this->ip = $request_stack
      ->getCurrentRequest()
      ->getClientIp();
    $this->agent = $request_stack
      ->getCurrentRequest()->headers
      ->get('User-Agent');
  }

  /**
   * Check if the given IP is a local IP-address.
   *
   * @return bool
   *   True if local IP; false otherwise.
   */
  private function isLocal() {
    if (preg_match('/^([012]?[0-9]{2})\\./', $this->ip, $match)) {
      switch ($match[1]) {
        case 10:
        case 127:
        case 172:
        case 192:
          return TRUE;
      }
    }
    return FALSE;
  }

  /**
   * Save IP address as a bot.
   */
  private function registerBot() {
    $this->database
      ->insert('rate_bot_ip')
      ->fields([
      'ip' => $this->ip,
    ])
      ->execute();
  }

  /**
   * Check if the IP-address exists in the local bot database.
   *
   * @return bool
   *   TRUE if IP is in database; false otherwise.
   */
  protected function checkIp() {
    return (bool) $this->database
      ->select('rate_bot_ip', 'rbi')
      ->fields('rbi', [
      'id',
    ])
      ->condition('rbi.ip', $this->ip)
      ->range(0, 1)
      ->execute()
      ->fetchField();
  }

  /**
   * Check if the given user agent matches the local bot database.
   *
   * @return bool
   *   True if match found; false otherwise.
   */
  protected function checkAgent() {
    $sql = 'SELECT 1 FROM {rate_bot_agent} WHERE :agent LIKE pattern LIMIT 1';
    return (bool) $this->database
      ->query($sql, [
      ':agent' => $this->agent,
    ])
      ->fetchField();
  }

  /**
   * Check the number of votes between now and $interval seconds ago.
   *
   * @param int $interval
   *   Interval in seconds.
   *
   * @return int
   *   Number of votes between not and internval.
   */
  protected function checkThreshold($interval) {
    $sql = 'SELECT COUNT(*) FROM {votingapi_vote} WHERE vote_source = :ip AND timestamp > :time';
    return $this->database
      ->query($sql, [
      ':ip' => $this->ip,
      ':time' => \Drupal::time()
        ->getRequestTime() - $interval,
    ])
      ->fetchField();
  }

  /**
   * Check if botscout thinks the IP is a bot.
   *
   * @return bool
   *   True if botscout returns a positive; false otherwise.
   */
  protected function checkBotscout() {
    $key = $this->config
      ->get('botscout_key');
    if ($key) {

      // @Todo: move to config.
      $uri = "http://botscout.com/test/?ip={$this->ip}&key={$key}";
      try {
        $response = $this->httpClient
          ->get($uri, [
          'headers' => [
            'Accept' => 'text/plain',
          ],
        ]);
        $data = (string) $response
          ->getBody();
        $status_code = $response
          ->getStatusCode();
        if (!empty($data) && $status_code == 200) {
          if (substr($data, 0, 1) === 'Y') {
            return TRUE;
          }
        }
      } catch (RequestException $e) {
        $this
          ->messenger()
          ->addMessage($this
          ->t('An error occurred contacting BotScout.'), 'warning');
        watchdog_exception('rate', $e);
      }
    }
    return FALSE;
  }

  /**
   * Check if the current user is blocked.
   *
   * This function will first check if the user is already known to be a bot.
   * If not, it will check if we have valid reasons to assume the user is a bot.
   *
   * @return bool
   *   True if bot detected; false otherwise.
   */
  public function checkIsBot() {
    if ($this
      ->isLocal()) {

      // The IP-address is a local IP-address. This is probably because of
      // misconfigured proxy servers. Do only the user agent check.
      return $this
        ->checkAgent();
    }
    if ($this
      ->checkIp()) {
      return TRUE;
    }
    if ($this
      ->checkAgent()) {

      // Identified as a bot by its user agent. Register this bot by IP-address
      // as well, in case this bots uses multiple agent strings.
      $this
        ->registerBot();
      return TRUE;
    }
    $threshold = $this->config
      ->get('bot_minute_threshold');
    if ($threshold && $this
      ->checkThreshold(60) > $threshold) {
      $this
        ->registerBot();
      return TRUE;
    }
    $threshold = $this->config
      ->get('bot_hour_threshold');

    // Always count, even if threshold is disabled. This is to determine if we
    // can skip the BotScout check.
    $count = $this
      ->checkThreshold(3600);
    if ($threshold && $count > $threshold) {
      $this
        ->registerBot();
      return TRUE;
    }
    if (!$count && $this
      ->checkBotscout()) {
      $this
        ->registerBot();
      return TRUE;
    }
    return FALSE;
  }

}

Classes

Namesort descending Description
RateBotDetector The rate.bot_detector service.