Избавляемся от спама на форумах phpBB
В связи с тем, что закрыл форум работающий на phpBB, хочу поделиться простым способом избавиться от спама, который проработал у меня пару лет и не давал сбоев..
После установки форума, я практически сразу начал получать тонны автоматического спама отправленного через хруммер и аналогичные программы. Сперва чистил все руками, но потом мне надоело и я решил немного усложнить жизнь спамерам.
Одним из критериев при борьбе со спамом для меня было не усложнить жизнь обычным пользователям. Я решил, что сперва просто модернизирую регистрацию, исходя из той мысли, что те кто массово спамят, не станут разбираться с конкретно моим кодом и следить за его изменениями.
Я написал javascript который после получения фокуса полем email, подменял его на другое поле. Дальше проверял пришло ли мое поле, на стороне сервера и если оно не пришло, то решал что это робот.
В файле: www/styles/prosilver/template/ucp_register.html, в конец файла, после формы, но перед
1 2 3 |
<!-- INCLUDE overall_footer.html --> |
добавляем такой скрипт
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
</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 в самое начало я добавил проверку
1 2 3 4 5 6 7 8 9 10 11 |
//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, вот его содержимое
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<?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-код в шаблоне, то помните, что шаблоны кешируются шаблонизатором и в момент отладки, надо чистить кеш. Сделать это можно или руками, или с помощью подключения этого файла.
Теперь, вернемся к тому, что же делать со спамом который постят вручную. Способов достаточно много, я выбрал способ с быстрым оповещением о любой активности на форуме, чтобы моментально узнавать не только о спаме, но и том что кто-то просит поддержки. Написал такой скрипт:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 |
<?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, в новых версиях код возможно потребует актуализации.
Author: | Tags: /
| Rating:
Leave a Reply