You are here

class RateBotDetector in Rate 8

The rate.bot_detector service.

Hierarchy

Expanded class hierarchy of RateBotDetector

1 string reference to 'RateBotDetector'
rate.services.yml in ./rate.services.yml
rate.services.yml
1 service uses RateBotDetector
rate.bot_detector in ./rate.services.yml
Drupal\rate\RateBotDetector

File

src/RateBotDetector.php, line 16

Namespace

Drupal\rate
View source
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;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
MessengerTrait::$messenger protected property The messenger. 29
MessengerTrait::messenger public function Gets the messenger. 29
MessengerTrait::setMessenger public function Sets the messenger.
RateBotDetector::$agent protected property HTTP User agent.
RateBotDetector::$config protected property The config factory wrapper to fetch settings.
RateBotDetector::$database protected property Database connection object.
RateBotDetector::$httpClient protected property The Http Client object.
RateBotDetector::$ip protected property Client IP.
RateBotDetector::$requestStack protected property The request stack.
RateBotDetector::checkAgent protected function Check if the given user agent matches the local bot database.
RateBotDetector::checkBotscout protected function Check if botscout thinks the IP is a bot.
RateBotDetector::checkIp protected function Check if the IP-address exists in the local bot database.
RateBotDetector::checkIsBot public function Check if the current user is blocked.
RateBotDetector::checkThreshold protected function Check the number of votes between now and $interval seconds ago.
RateBotDetector::isLocal private function Check if the given IP is a local IP-address.
RateBotDetector::registerBot private function Save IP address as a bot.
RateBotDetector::__construct public function RateBotDetector constructor.
StringTranslationTrait::$stringTranslation protected property The string translation service. 1
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.