X

Избавляемся от спама на форумах phpBB

В связи с тем, что закрыл форум работающий на phpBB, хочу поделиться простым способом избавиться от спама, который проработал у меня пару лет и не давал сбоев..

После установки форума, я практически сразу начал получать тонны автоматического спама отправленного через хруммер и аналогичные программы. Сперва чистил все руками, но потом мне надоело и я решил немного усложнить жизнь спамерам.

Одним из критериев при борьбе со спамом для меня было не усложнить жизнь обычным пользователям. Я решил, что сперва просто модернизирую регистрацию, исходя из той мысли, что те кто массово спамят, не станут разбираться с конкретно моим кодом и следить за его изменениями.

Я написал javascript который после получения фокуса полем email, подменял его на другое поле. Дальше проверял пришло ли мое поле, на стороне сервера и если оно не пришло, то решал что это робот.

В файле: www/styles/prosilver/template/ucp_register.html, в конец файла, после формы, но перед

<!-- INCLUDE overall_footer.html -->

добавляем такой скрипт

</form>

<script>
    var el = document.getElementById('email');
    el.onfocus = function(){
        el.style.display = 'none';
        var container = document.createElement('div');
        container.innerHTML = '<input name="emai1" id="emai1" type="text" tabindex="2" size="25" maxlength="100" value="{EMAIL}" class="inputbox autowidth" autocomplete="off">';
        el.parentNode.appendChild(container);
        document.getElementById('emai1').focus();
        document.forms["register"].onsubmit = function(){
            el.parentNode.removeChild(el);
        }
    };
</script>


<!-- INCLUDE overall_footer.html -->

Если присмотреться, то вы заметите, что при получении фокуса полем email (L на конце), создается поле emai1 (Единица на конце) и оригинальное поле подменяется на него.

Далее, в файле www/ucp.php в самое начало я добавил проверку

//include 'disable-cache.php';

if (!empty($_GET['mode']) && $_GET['mode'] =='register' && !empty($_POST['submit'])) {
    if (empty($_POST['emai1'])) {
        $_POST['email'] = '';
    } else {
        $_POST['email'] = $_POST['emai1'];
    }
}

Тут проверяется следующее, если была попытка регистрации, то проверяется наличие поля $_POST['emai1'] (Единица на конце) и если оно пришло, то его содержимое копируется в оригинальную переменную $_POST['email'] (L на конце). В противном случае, скрипт очищает любые данные переданные в $_POST['email'].

Усложнять сильнее можно развивая эту идею, но я не стал этого делать, т.к. только это отсеяло всех авто-ботов.

В коде выше вы можете увидеть закомментированную строку с подключением файла disable-cache.php, вот его содержимое

<?php

//die('disabled on backend');

function rrmdir($dir) {
   if (is_dir($dir)) {
     $objects = scandir($dir);
     foreach ($objects as $object) {
       if ($object != "." && $object != "..") {
         if (is_dir($dir."/".$object))
           rrmdir($dir."/".$object);
         else
           unlink($dir."/".$object);
       }
     }
     rmdir($dir);
   }
 }

 rrmdir(dirname(__FILE__).'/cache/twig');
 mkdir(dirname(__FILE__).'/cache/twig',0755);

 foreach(glob(dirname(__FILE__).'/cache/*.php') as $file) unlink($file);

Его смысл, в том, чтобы в момент отладки при каждой перезагрузки страницы очищать кеш файлов шаблонов. Если будете использовать мой код, то вам его не надо подключать. Если будете модернизировать js-код в шаблоне, то помните, что шаблоны кешируются шаблонизатором и в момент отладки, надо чистить кеш. Сделать это можно или руками, или с помощью подключения этого файла.

Теперь, вернемся к тому, что же делать со спамом который постят вручную. Способов достаточно много, я выбрал способ с быстрым оповещением о любой активности на форуме, чтобы моментально узнавать не только о спаме, но и том что кто-то просит поддержки. Написал такой скрипт:

<?php

    class Cfg {
        const DB_HOST = 'localhost';
        const DB_USER = '<ПОЛЬЗОВАТЕЛЬ-БД>';
        const DB_PASS = '<ПАРОЛЬ-БД>';
        const DB_NAME = '<ИМЯ-БД>';

        const EMAIL_FROM = 'It-Rem Forum <no-reply@forum.it-rem.ru>';
        const EMAIL_REPLY_TO = 'no-reply@forum.it-rem.ru';
        const EMAIL_TO = 'new@forum.it-rem.ru';
        const EMAIL_SUBJECT = 'New posts at forum.it-rem.ru';

        const POSTS_PER_MAIL_LIMIT = 50; // per email limit

        const ADMIN_TIMEZONE_OFFSET = 3; // in hours from UTC

        const PHPBB_TBL_PREFIX = 'phpbb_';
        const PHPBB_BASE_URL = 'http://forum.it-rem.ru'; // base url of forum, ex: http://forum.site.com

        const ALLOW_IGNORE_USERS = true; // that option allow ignore messages from specified users (admins, moderators, etc..)

        public static function getIgnoreUsersSql() {
            $ignoreUsersWithEmails = [ // specify here emails that we should ignore
                'admin@forum.it-rem.ru',
            ];

            if (!self::ALLOW_IGNORE_USERS OR empty($ignoreUsersWithEmails)) return '';

            return ' AND `users`.`user_email`!="'
                   .implode('" AND `users`.`user_email`!="', $ignoreUsersWithEmails)
                   .'"'
            ;
        }

        public static function getVar($var, $default=null) {
            $values = [];
            $storagePath = dirname(__FILE__).'/storage.json';
            if (file_exists($storagePath)) {
                $values = json_decode(file_get_contents($storagePath), true);
                if (!is_array($values)) $values =[];
            }
            return isset($values[$var]) ? $values[$var] : $default;
        }

        public static function setVar($var, $val) {
            $values = [];
            $storagePath = dirname(__FILE__).'/storage.json';
            if (file_exists($storagePath)) {
                $values = json_decode(file_get_contents($storagePath), true);
                if (!is_array($values)) $values =[];
            }
            $values[$var] = $val;
            file_put_contents($storagePath, json_encode($values));
        }

    }

    class DbConnection {
        private $host = '';
        private $user = '';
        private $pass = '';
        private $dbName = '';
        private $defaultCharset = 'utf8';
        private $dbLink = null;

        public function __construct($host, $user, $pass, $dbName) {
            $this->host = $host;
            $this->user = $user;
            $this->pass = $pass;
            $this->dbName = $dbName;
        }

        public function getConnection($force=false){
            if (is_null($this->dbLink) || $force) {
                $this->dbLink = new mysqli($this->host, $this->user, $this->pass, $this->dbName);

                if ($this->dbLink->connect_errno) {
                    echo "MySQL Error: Failed to make a MySQL connection, here is why: \n";
                    echo "Errno: " . $this->dbLink->connect_errno . "\n";
                    echo "Error: " . $this->dbLink->connect_error . "\n";
                    exit;
                }
            }

            if (!$this->dbLink->set_charset($this->defaultCharset)) {
                echo "MySQL Error loading character set $charset\n";
                echo "Errno: " . $this->dbLink->errno . "\n";
                echo "Error: " . $this->dbLink->error . "\n";
                exit;
            }

            return $this->dbLink;
        }

        public function setCharset($charset='utf8') {
            $db = $this->getConnection();
            if (!$db->set_charset($charset)){
                echo "MySQL Error loading character set $charset\n";
                echo "Errno: " . $db->errno . "\n";
                echo "Error: " . $db->error . "\n";
                exit;
            }
        }

        public function query($sql){
            $db = $this->getConnection();
            if (!$result = $db->query($sql)) {
                echo "MySQL Error: Our query failed to execute and here is why: \n";
                echo "Query: " . $sql . "\n";
                echo "Errno: " . $db->errno . "\n";
                echo "Error: " . $db->error . "\n";
                exit;
            }
            return $result;
        }

        public function select($sql){
            $result = $this->query($sql);
            $ret = [];
            if ($result->num_rows !== 0) {
                while ($row = $result->fetch_assoc()) {
                    $ret[] = $row;
                }
            }
            $result->free();
            return $ret;
        }

        public function close(){
            if (!is_null($this->dbLink)) {
                $this->dbLink->close();
            }
        }

        public function __destruct(){
            $this->close();
        }
    }

    $db = new DbConnection(Cfg::DB_HOST, Cfg::DB_USER, Cfg::DB_PASS, Cfg::DB_NAME);

    $lastSentPostId = Cfg::getVar('lastSentPostId',0);

    $sql = 'SELECT
                `posts`.`post_id` AS `post_id`,
                `posts`.`forum_id` AS `forum_id`,
                `posts`.`topic_id` AS `topic_id`,

                `posts`.`post_time` AS `time`,
                `posts`.`post_subject` AS `subject`,
                `posts`.`post_text` AS `text`,

                `users`.`username` AS `author`,
                `users`.`user_email` AS `email`
            FROM
                `'.Cfg::PHPBB_TBL_PREFIX.'posts` AS `posts`
            LEFT JOIN
                `'.Cfg::PHPBB_TBL_PREFIX.'users` AS `users`
            ON
                `posts`.`poster_id` = `users`.`user_id`
            WHERE
               `posts`.`post_id`>'.intval($lastSentPostId).'
               '.Cfg::getIgnoreUsersSql().'
            ORDER BY
                `posts`.`post_id` ASC
            LIMIT '.Cfg::POSTS_PER_MAIL_LIMIT.'
    ';

    $newPosts = $db->select($sql);

    if (!$newPosts) {
        echo 'No new posts from last check';
        exit;
    }

    $msgTemplate = '
    <html>
        <head>
            <style>
                div.text {
                    background-color: #F5F5F5;
                    font-size:13px;
                    margin:15px 0px;
                    max-width:600px;
                    border: 1px solid #D5D5D5;
                    border-left: 2px solid silver;
                    border-radius:5px;
                    padding:15px;
                }
                blockquote {
                    background-color: #d5d5d5;
                    border: 1px solid #c5c5c5;
                    padding: 5px;
                    color:black;
                    margin:0px 0px 0px 15px;
                    border-radius:2px;
                }
                blockquote blockquote {
                    background-color: #e4e4e4;
                    margin: 0.5em 1px 0 15px;
                }
                blockquote blockquote blockquote {
                    background-color: #f4f4f4;
                }
                .bbcode_tags {color:silver;}
            </style>
        </head>
        <body style="font-family:Courier; font-size:14px;">
            {body}
        </body>
    </html>
    ';

    $msgPostTemplate = '
        <strong>User</strong>: {author} &lt;{email}&gt;<br>
        <strong>Subject</strong>: {subject}<br>
        <strong>Date</strong>: {date}<br>
        <strong>Text</strong>:
            <div class="text">
                {text}
            </div>
        <strong>Manage</strong>: <a href="{forum_url}/viewtopic.php?f={forum_id}&t={topic_id}">Open topic</a><br>

        <br><hr><br>
    ';

    $msgBody = '';

    foreach($newPosts as $post) {

        $text = $post['text'];

        // replace quotes bbcode
        $text = preg_replace('~\[quote[^:\]]*:[^\]]+\]~','<blockquote>',$text);
        $text = preg_replace('~\[/quote:[^\]]+\]~','</blockquote>',$text);

        // replace code bbcode
        $text = preg_replace('~\[code[^:\]]*:[^\]]+\]~','<code>',$text);
        $text = preg_replace('~\[/code:[^\]]+\]~','</code>',$text);

        // replace b,i,u bbcodes
        $text = preg_replace('~\[(b|u|i):[^\]]+\]~','<$1>',$text);
        $text = preg_replace('~\[/(b|u|i):[^\]]+\]~','</$1>',$text);

        // replace urls bbcode
        $text = preg_replace('~\[url:[^\]]+\]([^\[]+)(?=\[)~','<a href="$1">$1',$text);
        $text = preg_replace('~\[url=([^:\]]+):[^\]]+\]([^\[]+)(?=\[)~','<a href="$1">$2',$text);
        $text = preg_replace('~\[/url:[^\]]+\]~','</a>',$text);

        // replace video bbcode
        $text = preg_replace('~\[\s?video:[^\]]+\]([^\[]+)(?=\[)~','<a href="$1">$1',$text);
        $text = preg_replace('~\[/video:[^\]]+\]~','</a>',$text);

        // replace other bbcode
        $text = preg_replace('~\[([^:\]]+):[^\]]+\]~','<span class="bbcode_tags">[$1]</span>',$text);

        // clean up double spaces and double line endings. Convert line ending to new lines (br)

        // $text = preg_replace('~(\s)\s+~','$1',$text);
        $text = str_replace("\n",'<br>', trim($text));

        // Fix unclosed tags
        $dom = new DOMDocument();
        $dom->loadHTML('<?xml encoding="utf-8" ?>'.$text, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
        $text = $dom->saveHTML();

        $msgBody .=  str_replace(
            [
                '{author}',
                '{email}',
                '{subject}',
                '{date}',
                '{text}',
                '{forum_url}',
                '{forum_id}',
                '{topic_id}',
            ], [
                htmlspecialchars($post['author']),
                $post['email'] ? htmlspecialchars($post['email']) : 'undefined@email',
                $post['subject'],
                date('d.m.y H:i',strtotime('+'.Cfg::ADMIN_TIMEZONE_OFFSET.' hours',$post['time'])),
                $text,
                Cfg::PHPBB_BASE_URL,
                intval($post['forum_id']),
                intval($post['topic_id']),
            ],
            $msgPostTemplate
        );
    }

    $emailMessage = str_replace('{body}',$msgBody, $msgTemplate);

    $emailheaders = "From: " . Cfg::EMAIL_FROM . "\r\n";
    $emailheaders .= "Reply-To: ". Cfg::EMAIL_REPLY_TO . "\r\n";
    $emailheaders .= "MIME-Version: 1.0\r\n";
    $emailheaders .= "Content-Type: text/html; charset=UTF-8\r\n";

    if (mail(Cfg::EMAIL_TO, Cfg::EMAIL_SUBJECT, $emailMessage, $emailheaders)) {
        Cfg::setVar('lastSentPostId', $newPosts[count($newPosts)-1]['post_id']);
        echo 'Message about '.count($newPosts).' new post was succesfully sent'.PHP_EOL;
    } else {
        echo 'Error: Mail not sent'.PHP_EOL;
    }

Этот код надо сохранить в какую-нибудь отдельную папку, напримеру www/notifier/cron.php и добавить в Cron, на запуск каждые 15 минут. В результате на указанный email будт приходить в виде писем все опубликованные посты. Это позволяет видеть сразу что и в каком форуме опубликовали и вовремя отреагировать - дать ответ или забанить спамера. Для форумов у которых очень много ежедневных сообщений это всего скорее не подойдет, в таком виде, надо дописать и ввести какие-нибудь ограничения. А вот для небольших сообществ с одним можератором будет самое то.

P.S. Весь описанный в этой статье код был актуален для phpBB 3.1.9, в новых версиях код возможно потребует актуализации.

Категории: phpBB
Тэги: phpbb