В связи с тем, что закрыл форум работающий на 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} <{email}><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, в новых версиях код возможно потребует актуализации.