mirror of
https://github.com/vichan-devel/vichan.git
synced 2024-11-23 23:20:57 +01:00
Improved Tinyboard anti-bot/spam filter. See large comment in inc/config.php for details.
This commit is contained in:
parent
dd0f421015
commit
a564a95ab4
@ -12,8 +12,7 @@ if(realpath($_SERVER['SCRIPT_FILENAME']) == str_replace('\\', '/', __FILE__)) {
|
||||
$hidden_inputs_twig = array();
|
||||
|
||||
class AntiBot {
|
||||
public $inputs = array(), $index = 0;
|
||||
private $salt;
|
||||
public $salt, $inputs = array(), $index = 0;
|
||||
|
||||
public static function randomString($length, $uppercase = false, $special_chars = false) {
|
||||
$chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
@ -90,12 +89,14 @@ class AntiBot {
|
||||
$this->inputs[$name] = (string)rand(0, 100);
|
||||
} else {
|
||||
// Obscure value
|
||||
$this->inputs[$name] = $this->randomString(rand(5, 100));
|
||||
$this->inputs[$name] = $this->randomString(rand(5, 100), true, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function html($count = false) {
|
||||
global $config;
|
||||
|
||||
$elements = array(
|
||||
'<input type="hidden" name="%name%" value="%value%">',
|
||||
'<input type="hidden" value="%value%" name="%name%">',
|
||||
@ -110,7 +111,11 @@ class AntiBot {
|
||||
|
||||
$html = '';
|
||||
|
||||
if($count == 0) {
|
||||
if($count === false) {
|
||||
$count = rand(1, count($this->inputs) / 15);
|
||||
}
|
||||
|
||||
if($count === true) {
|
||||
// all elements
|
||||
$inputs = array_slice($this->inputs, $this->index);
|
||||
} else {
|
||||
@ -135,6 +140,9 @@ class AntiBot {
|
||||
else
|
||||
$value = utf8tohtml($value);
|
||||
|
||||
if(strpos($element, 'textarea') === false)
|
||||
$value = str_replace('"', '"', $value);
|
||||
|
||||
$element = str_replace('%value%', $value, $element);
|
||||
|
||||
$html .= $element;
|
||||
@ -162,36 +170,42 @@ class AntiBot {
|
||||
// Use SHA1 for the hash
|
||||
return sha1($hash . $this->salt);
|
||||
}
|
||||
};;
|
||||
|
||||
|
||||
function hiddenInputs(array $salt, $print_the_rest = false) {
|
||||
global $hidden_inputs_twig;
|
||||
|
||||
$salt_str = implode(':', $salt);
|
||||
|
||||
if(!isset($hidden_inputs_twig[$salt_str]))
|
||||
$hidden_inputs_twig[$salt_str] = new AntiBot($salt);
|
||||
|
||||
if($print_the_rest)
|
||||
return $hidden_inputs_twig[$salt_str]->html(0);
|
||||
else
|
||||
return $hidden_inputs_twig[$salt_str]->html(rand(1, 5));
|
||||
}
|
||||
|
||||
function hiddenInputsHash(array $salt) {
|
||||
global $hidden_inputs_twig;
|
||||
function _create_antibot($board, $thread) {
|
||||
global $config;
|
||||
|
||||
$salt_str = implode(':', $salt);
|
||||
$antibot = new AntiBot(array($board, $thread));
|
||||
|
||||
if(!isset($hidden_inputs_twig[$salt_str]))
|
||||
$hidden_inputs_twig[$salt_str] = new AntiBot($salt);
|
||||
query('DELETE FROM `antispam` WHERE `expires` < UNIX_TIMESTAMP()') or error(db_error($query));
|
||||
|
||||
return $hidden_inputs_twig[$salt_str]->hash();
|
||||
if($thread)
|
||||
$query = prepare('UPDATE `antispam` SET `expires` = UNIX_TIMESTAMP() + :expires WHERE `board` = :board AND `thread` = :thread');
|
||||
else
|
||||
$query = prepare('UPDATE `antispam` SET `expires` = UNIX_TIMESTAMP() + :expires WHERE `board` = :board AND `thread` IS NULL');
|
||||
|
||||
$query->bindValue(':board', $board);
|
||||
if($thread)
|
||||
$query->bindValue(':thread', $thread);
|
||||
$query->bindValue(':expires', $config['spam']['hidden_inputs_expire']);
|
||||
$query->execute() or error(db_error($query));
|
||||
|
||||
$query = prepare('INSERT INTO `antispam` VALUES (:board, :thread, CRC32(:hash), UNIX_TIMESTAMP(), NULL, 0)');
|
||||
$query->bindValue(':board', $board);
|
||||
$query->bindValue(':thread', $thread);
|
||||
$query->bindValue(':hash', $antibot->hash());
|
||||
$query->execute() or error(db_error($query));
|
||||
|
||||
if($query->rowCount() == 0) {
|
||||
// there was no database entry for this hash. most likely expired.
|
||||
return true;
|
||||
}
|
||||
|
||||
return $antibot;
|
||||
}
|
||||
|
||||
function checkSpam(array $extra_salt = array()) {
|
||||
global $config;
|
||||
global $config, $pdo;
|
||||
|
||||
if(!isset($_POST['hash']))
|
||||
return true;
|
||||
@ -231,6 +245,25 @@ function checkSpam(array $extra_salt = array()) {
|
||||
// Use SHA1 for the hash
|
||||
$_hash = sha1($_hash . $extra_salt);
|
||||
|
||||
return $hash != $_hash;
|
||||
if($hash != $_hash)
|
||||
return true;
|
||||
|
||||
$query = prepare('UPDATE `antispam` SET `passed` = `passed` + 1 WHERE `hash` = CRC32(:hash)');
|
||||
$query->bindValue(':hash', $hash);
|
||||
$query->execute() or error(db_error($query));
|
||||
if($query->rowCount() == 0) {
|
||||
// there was no database entry for this hash. most likely expired.
|
||||
return true;
|
||||
}
|
||||
|
||||
$query = prepare('SELECT `passed` FROM `antispam` WHERE `hash` = CRC32(:hash)');
|
||||
$query->bindValue(':hash', $hash);
|
||||
$query->execute() or error(db_error($query));
|
||||
$passed = $query->fetchColumn(0);
|
||||
|
||||
if($passed > $config['spam']['hidden_inputs_max_pass'])
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -170,9 +170,33 @@
|
||||
// Skip checking certain IP addresses against blacklists (for troubleshooting or whatever)
|
||||
$config['dnsbl_exceptions'][] = '127.0.0.1';
|
||||
|
||||
// Spam filter
|
||||
/*
|
||||
* Introduction to Tinyboard's spam filter:
|
||||
*
|
||||
* In simple terms, whenever a posting form on a page is generated (which happens whenever a
|
||||
* post is made), Tinyboard will add a random amount of hidden, obscure fields to it to
|
||||
* confuse bots and upset hackers. These fields and their respective obscure values are
|
||||
* validated upon posting with a 160-bit "hash". That hash can only be used as many times
|
||||
* as you specify; otherwise, flooding bots could just keep reusing the same hash.
|
||||
* Once a new set of inputs (and the hash) are generated, old hashes for the same thread
|
||||
* and board are set to expire. Because you have to reload the page to get the new set
|
||||
* of inputs and hash, if they expire too quickly and more than one person is viewing the
|
||||
* page at a given time, Tinyboard would return false positives (depending on how long the
|
||||
* user sits on the page before posting). If your imageboard is quite fast/popular, set
|
||||
* $config['spam']['hidden_inputs_max_pass'] and $config['spam']['hidden_inputs_expire'] to
|
||||
* something higher to avoid false positives.
|
||||
*
|
||||
* See also: http://tinyboard.org/docs/?p=Your_request_looks_automated
|
||||
*
|
||||
*/
|
||||
|
||||
// Number of hidden fields to generate
|
||||
$config['spam']['hidden_inputs_min'] = 4;
|
||||
$config['spam']['hidden_inputs_max'] = 12;
|
||||
// How many times can a "hash" be used to post?
|
||||
$config['spam']['hidden_inputs_max_pass'] = 30;
|
||||
// How soon after regeneration do hashes expire (in seconds)?
|
||||
$config['spam']['hidden_inputs_expire'] = 60 * 60 * 2; // two hours
|
||||
// These are fields used to confuse the bots. Make sure they aren't actually used by Tinyboard, or it won't work.
|
||||
$config['spam']['hidden_input_names'] = array(
|
||||
'user',
|
||||
|
@ -13,7 +13,6 @@ require_once 'inc/display.php';
|
||||
require_once 'inc/template.php';
|
||||
require_once 'inc/database.php';
|
||||
require_once 'inc/events.php';
|
||||
require_once 'inc/anti-bot.php';
|
||||
require_once 'inc/lib/gettext/gettext.inc';
|
||||
|
||||
// the user is not currently logged in as a moderator
|
||||
@ -210,6 +209,11 @@ function _syslog($priority, $message) {
|
||||
}
|
||||
}
|
||||
|
||||
function create_antibot($board, $thread = null) {
|
||||
require_once 'inc/anti-bot.php';
|
||||
|
||||
return _create_antibot($board, $thread);
|
||||
}
|
||||
|
||||
function rebuildThemes($action) {
|
||||
// List themes
|
||||
@ -1175,6 +1179,7 @@ function buildIndex() {
|
||||
$content['pages'] = $pages;
|
||||
$content['pages'][$page-1]['selected'] = true;
|
||||
$content['btn'] = getPageButtons($content['pages']);
|
||||
$content['antibot'] = create_antibot($board['uri']);
|
||||
file_write($filename, Element('index.html', $content));
|
||||
|
||||
if(isset($md5) && $md5 == md5_file($filename)) {
|
||||
@ -1492,6 +1497,7 @@ function buildThread($id, $return=false, $mod=false) {
|
||||
'config' => $config,
|
||||
'id' => $id,
|
||||
'mod' => $mod,
|
||||
'antibot' => $mod ? false : create_antibot($board['uri'], $id),
|
||||
'boardlist' => createBoardlist($mod),
|
||||
'return' => ($mod ? '?' . $board['url'] . $config['file_index'] : $config['root'] . $board['uri'] . '/' . $config['file_index'])
|
||||
));
|
||||
|
@ -24,7 +24,7 @@ function load_twig() {
|
||||
|
||||
$loader = new Twig_Loader_Filesystem($config['dir']['template']);
|
||||
$loader->setPaths($config['dir']['template']);
|
||||
$twig = new Twig_Environment($loader, Array(
|
||||
$twig = new Twig_Environment($loader, array(
|
||||
'autoescape' => false,
|
||||
'cache' => "{$config['dir']['template']}/cache",
|
||||
'debug' => ($config['debug'] ? true : false),
|
||||
|
13
install.php
13
install.php
@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
// Installation/upgrade file
|
||||
define('VERSION', 'v0.9.6-dev-1');
|
||||
define('VERSION', 'v0.9.6-dev-2');
|
||||
|
||||
require 'inc/functions.php';
|
||||
|
||||
@ -174,6 +174,17 @@ if(file_exists($config['has_installed'])) {
|
||||
CHANGE `uri` `uri` VARCHAR( 50 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
|
||||
CHANGE `title` `title` TINYTEXT CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
|
||||
CHANGE `subtitle` `subtitle` TINYTEXT CHARACTER SET utf8 COLLATE utf8_general_ci NULL") or error(db_error());
|
||||
case 'v0.9.6-dev-1':
|
||||
query("CREATE TABLE IF NOT EXISTS `antispam` (
|
||||
`board` varchar(255) NOT NULL,
|
||||
`thread` int(11) DEFAULT NULL,
|
||||
`hash` bigint(20) NOT NULL,
|
||||
`created` int(11) NOT NULL,
|
||||
`expires` int(11) DEFAULT NULL,
|
||||
`passed` smallint(6) NOT NULL,
|
||||
PRIMARY KEY (`hash`),
|
||||
KEY `board` (`board`,`thread`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8;");
|
||||
case false:
|
||||
// Update version number
|
||||
file_write($config['has_installed'], VERSION);
|
||||
|
17
install.sql
17
install.sql
@ -228,6 +228,23 @@ CREATE TABLE IF NOT EXISTS `cites` (
|
||||
KEY `post` (`board`,`post`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `antispam`
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `antispam` (
|
||||
`board` varchar(255) NOT NULL,
|
||||
`thread` int(11) DEFAULT NULL,
|
||||
`hash` bigint(20) NOT NULL,
|
||||
`created` int(11) NOT NULL,
|
||||
`expires` int(11) DEFAULT NULL,
|
||||
`passed` smallint(6) NOT NULL,
|
||||
PRIMARY KEY (`hash`),
|
||||
KEY `board` (`board`,`thread`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
|
||||
|
||||
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
|
10
mod.php
10
mod.php
@ -9,7 +9,7 @@ require 'inc/mod.php';
|
||||
|
||||
if (get_magic_quotes_gpc()) {
|
||||
function strip_array($var) {
|
||||
return is_array($var) ? array_map("strip_array", $var) : stripslashes($var);
|
||||
return is_array($var) ? array_map('strip_array', $var) : stripslashes($var);
|
||||
}
|
||||
|
||||
$_GET = strip_array($_GET);
|
||||
@ -1698,6 +1698,10 @@ if(!$mod) {
|
||||
$query->bindValue(':board', $board['uri']);
|
||||
$query->execute() or error(db_error($query));
|
||||
|
||||
$query = prepare("DELETE FROM `antispam` WHERE `board` = :board");
|
||||
$query->bindValue(':board', $board['uri']);
|
||||
$query->execute() or error(db_error($query));
|
||||
|
||||
$_board = $board;
|
||||
|
||||
rebuildThemes('boards');
|
||||
@ -2209,7 +2213,6 @@ if(!$mod) {
|
||||
$page['pages'][$page_no-1]['selected'] = true;
|
||||
$page['btn'] = getPageButtons($page['pages'], true);
|
||||
$page['mod'] = true;
|
||||
|
||||
echo Element('index.html', $page);
|
||||
} elseif(preg_match('/^\/' . $regex['board'] . $regex['res'] . $regex['page'] . '$/', $query, $matches)) {
|
||||
// View thread
|
||||
@ -2354,7 +2357,8 @@ if(!$mod) {
|
||||
if(!openBoard($boardName))
|
||||
error($config['error']['noboard']);
|
||||
|
||||
if(!hasPermission($config['mod']['delete'], $boardName)) error($config['error']['noaccess']);
|
||||
if(!hasPermission($config['mod']['delete'], $boardName))
|
||||
error($config['error']['noaccess']);
|
||||
|
||||
$post = &$matches[2];
|
||||
|
||||
|
43
post.php
43
post.php
@ -5,11 +5,12 @@
|
||||
*/
|
||||
|
||||
require 'inc/functions.php';
|
||||
require 'inc/anti-bot.php';
|
||||
|
||||
// Fix for magic quotes
|
||||
if (get_magic_quotes_gpc()) {
|
||||
function strip_array($var) {
|
||||
return is_array($var) ? array_map("strip_array", $var) : stripslashes($var);
|
||||
return is_array($var) ? array_map('strip_array', $var) : stripslashes($var);
|
||||
}
|
||||
|
||||
$_GET = strip_array($_GET);
|
||||
@ -192,7 +193,26 @@ if(isset($_POST['delete'])) {
|
||||
}
|
||||
}
|
||||
|
||||
if(checkSpam(array($board['uri'], isset($post['thread']) && !($config['quick_reply'] && isset($_POST['quick-reply'])) ? $post['thread'] : null)))
|
||||
if($post['mod'] = isset($_POST['mod']) && $_POST['mod']) {
|
||||
require 'inc/mod.php';
|
||||
if(!$mod) {
|
||||
// Liar. You're not a mod.
|
||||
error($config['error']['notamod']);
|
||||
}
|
||||
|
||||
$post['sticky'] = $post['op'] && isset($_POST['sticky']);
|
||||
$post['locked'] = $post['op'] && isset($_POST['lock']);
|
||||
$post['raw'] = isset($_POST['raw']);
|
||||
|
||||
if($post['sticky'] && !hasPermission($config['mod']['sticky'], $board['uri']))
|
||||
error($config['error']['noaccess']);
|
||||
if($post['locked'] && !hasPermission($config['mod']['lock'], $board['uri']))
|
||||
error($config['error']['noaccess']);
|
||||
if($post['raw'] && !hasPermission($config['mod']['rawhtml'], $board['uri']))
|
||||
error($config['error']['noaccess']);
|
||||
}
|
||||
|
||||
if(!$post['mod'] && checkSpam(array($board['uri'], isset($post['thread']) && !($config['quick_reply'] && isset($_POST['quick-reply'])) ? $post['thread'] : null)))
|
||||
error($config['error']['spam']);
|
||||
|
||||
if($config['robot_enable'] && $config['robot_mute']) {
|
||||
@ -239,25 +259,6 @@ if(isset($_POST['delete'])) {
|
||||
}
|
||||
}
|
||||
|
||||
if($post['mod'] = isset($_POST['mod']) && $_POST['mod']) {
|
||||
require 'inc/mod.php';
|
||||
if(!$mod) {
|
||||
// Liar. You're not a mod.
|
||||
error($config['error']['notamod']);
|
||||
}
|
||||
|
||||
$post['sticky'] = $post['op'] && isset($_POST['sticky']);
|
||||
$post['locked'] = $post['op'] && isset($_POST['lock']);
|
||||
$post['raw'] = isset($_POST['raw']);
|
||||
|
||||
if($post['sticky'] && !hasPermission($config['mod']['sticky'], $board['uri']))
|
||||
error($config['error']['noaccess']);
|
||||
if($post['locked'] && !hasPermission($config['mod']['lock'], $board['uri']))
|
||||
error($config['error']['noaccess']);
|
||||
if($post['raw'] && !hasPermission($config['mod']['rawhtml'], $board['uri']))
|
||||
error($config['error']['noaccess']);
|
||||
}
|
||||
|
||||
if(!hasPermission($config['mod']['bypass_field_disable'], $board['uri'])) {
|
||||
if($config['field_disable_name'])
|
||||
$_POST['name'] = $config['anonymous']; // "forced anonymous"
|
||||
|
@ -1,35 +1,35 @@
|
||||
<form name="post" onsubmit="return dopost(this);" enctype="multipart/form-data" action="{{ config.post_url }}" method="post">
|
||||
{{ hiddenInputs([board.uri, id]) }}
|
||||
{{ antibot.html() }}
|
||||
{% if id %}<input type="hidden" name="thread" value="{{ id }}">{% endif %}
|
||||
{{ hiddenInputs([board.uri, id]) }}
|
||||
{{ antibot.html() }}
|
||||
<input type="hidden" name="board" value="{{ board.uri }}">
|
||||
{{ hiddenInputs([board.uri, id]) }}
|
||||
{{ antibot.html() }}
|
||||
{% if mod %}<input type="hidden" name="mod" value="1">{% endif %}
|
||||
<table>
|
||||
{% if not config.field_disable_name or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri)) %}<tr>
|
||||
<th>
|
||||
{% trans %}Name{% endtrans %}
|
||||
{{ hiddenInputs([board.uri, id]) }}
|
||||
{{ antibot.html() }}
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" name="name" size="25" maxlength="50" autocomplete="off">
|
||||
{{ hiddenInputs([board.uri, id]) }}
|
||||
{{ antibot.html() }}
|
||||
</td>
|
||||
</tr>{% endif %}
|
||||
{% if not config.field_disable_email or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri)) %}<tr>
|
||||
<th>
|
||||
{% trans %}Email{% endtrans %}
|
||||
{{ hiddenInputs([board.uri, id]) }}
|
||||
{{ antibot.html() }}
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" name="email" size="25" maxlength="40" autocomplete="off">
|
||||
{{ hiddenInputs([board.uri, id]) }}
|
||||
{{ antibot.html() }}
|
||||
</td>
|
||||
</tr>{% endif %}
|
||||
<tr>
|
||||
<th>
|
||||
{% trans %}Subject{% endtrans %}
|
||||
{{ hiddenInputs([board.uri, id]) }}
|
||||
{{ antibot.html() }}
|
||||
</th>
|
||||
<td>
|
||||
<input style="float:left;" type="text" name="subject" size="25" maxlength="100" autocomplete="off">
|
||||
@ -39,22 +39,22 @@
|
||||
<tr>
|
||||
<th>
|
||||
{% trans %}Comment{% endtrans %}
|
||||
{{ hiddenInputs([board.uri, id]) }}
|
||||
{{ antibot.html() }}
|
||||
</th>
|
||||
<td>
|
||||
<textarea name="body" id="body" rows="5" cols="35"></textarea>
|
||||
{{ hiddenInputs([board.uri, id]) }}
|
||||
{{ antibot.html() }}
|
||||
</td>
|
||||
</tr>
|
||||
{% if config.recaptcha %}
|
||||
<tr>
|
||||
<th>
|
||||
{% trans %}Verification{% endtrans %}
|
||||
{{ hiddenInputs([board.uri, id]) }}
|
||||
{{ antibot.html() }}
|
||||
</th>
|
||||
<td>
|
||||
<script type="text/javascript" src="http://www.google.com/recaptcha/api/challenge?k={{ config.recaptcha_public }}"></script>
|
||||
{{ hiddenInputs([board.uri, id]) }}
|
||||
{{ antibot.html() }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
@ -64,18 +64,18 @@
|
||||
</th>
|
||||
<td>
|
||||
<input type="file" name="file">
|
||||
{{ hiddenInputs([board.uri, id]) }}
|
||||
{{ antibot.html() }}
|
||||
</td>
|
||||
</tr>
|
||||
{% if config.enable_embedding %}
|
||||
<tr>
|
||||
<th>
|
||||
{% trans %}Embed{% endtrans %}
|
||||
{{ hiddenInputs([board.uri, id]) }}
|
||||
{{ antibot.html() }}
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" name="embed" size="30" maxlength="120" autocomplete="off">
|
||||
{{ hiddenInputs([board.uri, id]) }}
|
||||
{{ antibot.html() }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
@ -103,18 +103,17 @@
|
||||
{% if not config.field_disable_password or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri)) %}<tr>
|
||||
<th>
|
||||
{% trans %}Password{% endtrans %}
|
||||
{{ hiddenInputs([board.uri, id]) }}
|
||||
{{ antibot.html() }}
|
||||
</th>
|
||||
<td>
|
||||
<input type="password" name="password" size="12" maxlength="18" autocomplete="off">
|
||||
<span class="unimportant">{% trans %}(For file deletion.){% endtrans %}</span>
|
||||
{{ hiddenInputs([board.uri, id]) }}
|
||||
{{ antibot.html() }}
|
||||
</td>
|
||||
</tr>{% endif %}
|
||||
</table>
|
||||
{{ hiddenInputs([board.uri, id]) }}
|
||||
<input type="hidden" name="hash" value="{{ hiddenInputsHash([board.uri, id]) }}">
|
||||
{{ hiddenInputs([board.uri, id], true) }}
|
||||
{{ antibot.html(true) }}
|
||||
<input type="hidden" name="hash" value="{{ antibot.hash() }}">
|
||||
</form>
|
||||
|
||||
<script type="text/javascript">{% raw %}
|
||||
|
Loading…
Reference in New Issue
Block a user