1
0
mirror of https://github.com/vichan-devel/vichan.git synced 2024-11-27 17:00:52 +01:00

Improved Tinyboard anti-bot/spam filter. See large comment in inc/config.php for details.

This commit is contained in:
Michael Save 2012-04-12 21:56:01 +10:00
parent dd0f421015
commit a564a95ab4
9 changed files with 171 additions and 76 deletions

View File

@ -12,8 +12,7 @@ if(realpath($_SERVER['SCRIPT_FILENAME']) == str_replace('\\', '/', __FILE__)) {
$hidden_inputs_twig = array(); $hidden_inputs_twig = array();
class AntiBot { class AntiBot {
public $inputs = array(), $index = 0; public $salt, $inputs = array(), $index = 0;
private $salt;
public static function randomString($length, $uppercase = false, $special_chars = false) { public static function randomString($length, $uppercase = false, $special_chars = false) {
$chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; $chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
@ -90,12 +89,14 @@ class AntiBot {
$this->inputs[$name] = (string)rand(0, 100); $this->inputs[$name] = (string)rand(0, 100);
} else { } else {
// Obscure value // 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) { public function html($count = false) {
global $config;
$elements = array( $elements = array(
'<input type="hidden" name="%name%" value="%value%">', '<input type="hidden" name="%name%" value="%value%">',
'<input type="hidden" value="%value%" name="%name%">', '<input type="hidden" value="%value%" name="%name%">',
@ -110,7 +111,11 @@ class AntiBot {
$html = ''; $html = '';
if($count == 0) { if($count === false) {
$count = rand(1, count($this->inputs) / 15);
}
if($count === true) {
// all elements // all elements
$inputs = array_slice($this->inputs, $this->index); $inputs = array_slice($this->inputs, $this->index);
} else { } else {
@ -134,7 +139,10 @@ class AntiBot {
$value = $this->make_confusing($value); $value = $this->make_confusing($value);
else else
$value = utf8tohtml($value); $value = utf8tohtml($value);
if(strpos($element, 'textarea') === false)
$value = str_replace('"', '&quot;', $value);
$element = str_replace('%value%', $value, $element); $element = str_replace('%value%', $value, $element);
$html .= $element; $html .= $element;
@ -162,36 +170,42 @@ class AntiBot {
// Use SHA1 for the hash // Use SHA1 for the hash
return sha1($hash . $this->salt); 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) { function _create_antibot($board, $thread) {
global $hidden_inputs_twig; global $config;
$salt_str = implode(':', $salt); $antibot = new AntiBot(array($board, $thread));
if(!isset($hidden_inputs_twig[$salt_str])) query('DELETE FROM `antispam` WHERE `expires` < UNIX_TIMESTAMP()') or error(db_error($query));
$hidden_inputs_twig[$salt_str] = new AntiBot($salt);
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()) { function checkSpam(array $extra_salt = array()) {
global $config; global $config, $pdo;
if(!isset($_POST['hash'])) if(!isset($_POST['hash']))
return true; return true;
@ -231,6 +245,25 @@ function checkSpam(array $extra_salt = array()) {
// Use SHA1 for the hash // Use SHA1 for the hash
$_hash = sha1($_hash . $extra_salt); $_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;
} }

View File

@ -170,9 +170,33 @@
// Skip checking certain IP addresses against blacklists (for troubleshooting or whatever) // Skip checking certain IP addresses against blacklists (for troubleshooting or whatever)
$config['dnsbl_exceptions'][] = '127.0.0.1'; $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_min'] = 4;
$config['spam']['hidden_inputs_max'] = 12; $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. // 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( $config['spam']['hidden_input_names'] = array(
'user', 'user',

View File

@ -13,7 +13,6 @@ require_once 'inc/display.php';
require_once 'inc/template.php'; require_once 'inc/template.php';
require_once 'inc/database.php'; require_once 'inc/database.php';
require_once 'inc/events.php'; require_once 'inc/events.php';
require_once 'inc/anti-bot.php';
require_once 'inc/lib/gettext/gettext.inc'; require_once 'inc/lib/gettext/gettext.inc';
// the user is not currently logged in as a moderator // 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) { function rebuildThemes($action) {
// List themes // List themes
@ -1175,6 +1179,7 @@ function buildIndex() {
$content['pages'] = $pages; $content['pages'] = $pages;
$content['pages'][$page-1]['selected'] = true; $content['pages'][$page-1]['selected'] = true;
$content['btn'] = getPageButtons($content['pages']); $content['btn'] = getPageButtons($content['pages']);
$content['antibot'] = create_antibot($board['uri']);
file_write($filename, Element('index.html', $content)); file_write($filename, Element('index.html', $content));
if(isset($md5) && $md5 == md5_file($filename)) { if(isset($md5) && $md5 == md5_file($filename)) {
@ -1492,6 +1497,7 @@ function buildThread($id, $return=false, $mod=false) {
'config' => $config, 'config' => $config,
'id' => $id, 'id' => $id,
'mod' => $mod, 'mod' => $mod,
'antibot' => $mod ? false : create_antibot($board['uri'], $id),
'boardlist' => createBoardlist($mod), 'boardlist' => createBoardlist($mod),
'return' => ($mod ? '?' . $board['url'] . $config['file_index'] : $config['root'] . $board['uri'] . '/' . $config['file_index']) 'return' => ($mod ? '?' . $board['url'] . $config['file_index'] : $config['root'] . $board['uri'] . '/' . $config['file_index'])
)); ));

View File

@ -24,7 +24,7 @@ function load_twig() {
$loader = new Twig_Loader_Filesystem($config['dir']['template']); $loader = new Twig_Loader_Filesystem($config['dir']['template']);
$loader->setPaths($config['dir']['template']); $loader->setPaths($config['dir']['template']);
$twig = new Twig_Environment($loader, Array( $twig = new Twig_Environment($loader, array(
'autoescape' => false, 'autoescape' => false,
'cache' => "{$config['dir']['template']}/cache", 'cache' => "{$config['dir']['template']}/cache",
'debug' => ($config['debug'] ? true : false), 'debug' => ($config['debug'] ? true : false),

View File

@ -1,7 +1,7 @@
<?php <?php
// Installation/upgrade file // Installation/upgrade file
define('VERSION', 'v0.9.6-dev-1'); define('VERSION', 'v0.9.6-dev-2');
require 'inc/functions.php'; 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 `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 `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()); 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: case false:
// Update version number // Update version number
file_write($config['has_installed'], VERSION); file_write($config['has_installed'], VERSION);

View File

@ -228,6 +228,23 @@ CREATE TABLE IF NOT EXISTS `cites` (
KEY `post` (`board`,`post`) KEY `post` (`board`,`post`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8; ) 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_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;

10
mod.php
View File

@ -9,7 +9,7 @@ require 'inc/mod.php';
if (get_magic_quotes_gpc()) { if (get_magic_quotes_gpc()) {
function strip_array($var) { 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); $_GET = strip_array($_GET);
@ -1698,6 +1698,10 @@ if(!$mod) {
$query->bindValue(':board', $board['uri']); $query->bindValue(':board', $board['uri']);
$query->execute() or error(db_error($query)); $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; $_board = $board;
rebuildThemes('boards'); rebuildThemes('boards');
@ -2209,7 +2213,6 @@ if(!$mod) {
$page['pages'][$page_no-1]['selected'] = true; $page['pages'][$page_no-1]['selected'] = true;
$page['btn'] = getPageButtons($page['pages'], true); $page['btn'] = getPageButtons($page['pages'], true);
$page['mod'] = true; $page['mod'] = true;
echo Element('index.html', $page); echo Element('index.html', $page);
} elseif(preg_match('/^\/' . $regex['board'] . $regex['res'] . $regex['page'] . '$/', $query, $matches)) { } elseif(preg_match('/^\/' . $regex['board'] . $regex['res'] . $regex['page'] . '$/', $query, $matches)) {
// View thread // View thread
@ -2354,7 +2357,8 @@ if(!$mod) {
if(!openBoard($boardName)) if(!openBoard($boardName))
error($config['error']['noboard']); 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]; $post = &$matches[2];

View File

@ -5,11 +5,12 @@
*/ */
require 'inc/functions.php'; require 'inc/functions.php';
require 'inc/anti-bot.php';
// Fix for magic quotes // Fix for magic quotes
if (get_magic_quotes_gpc()) { if (get_magic_quotes_gpc()) {
function strip_array($var) { 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); $_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']); error($config['error']['spam']);
if($config['robot_enable'] && $config['robot_mute']) { 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(!hasPermission($config['mod']['bypass_field_disable'], $board['uri'])) {
if($config['field_disable_name']) if($config['field_disable_name'])
$_POST['name'] = $config['anonymous']; // "forced anonymous" $_POST['name'] = $config['anonymous']; // "forced anonymous"

View File

@ -1,35 +1,35 @@
<form name="post" onsubmit="return dopost(this);" enctype="multipart/form-data" action="{{ config.post_url }}" method="post"> <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 %} {% if id %}<input type="hidden" name="thread" value="{{ id }}">{% endif %}
{{ hiddenInputs([board.uri, id]) }} {{ antibot.html() }}
<input type="hidden" name="board" value="{{ board.uri }}"> <input type="hidden" name="board" value="{{ board.uri }}">
{{ hiddenInputs([board.uri, id]) }} {{ antibot.html() }}
{% if mod %}<input type="hidden" name="mod" value="1">{% endif %} {% if mod %}<input type="hidden" name="mod" value="1">{% endif %}
<table> <table>
{% if not config.field_disable_name or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri)) %}<tr> {% if not config.field_disable_name or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri)) %}<tr>
<th> <th>
{% trans %}Name{% endtrans %} {% trans %}Name{% endtrans %}
{{ hiddenInputs([board.uri, id]) }} {{ antibot.html() }}
</th> </th>
<td> <td>
<input type="text" name="name" size="25" maxlength="50" autocomplete="off"> <input type="text" name="name" size="25" maxlength="50" autocomplete="off">
{{ hiddenInputs([board.uri, id]) }} {{ antibot.html() }}
</td> </td>
</tr>{% endif %} </tr>{% endif %}
{% if not config.field_disable_email or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri)) %}<tr> {% if not config.field_disable_email or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri)) %}<tr>
<th> <th>
{% trans %}Email{% endtrans %} {% trans %}Email{% endtrans %}
{{ hiddenInputs([board.uri, id]) }} {{ antibot.html() }}
</th> </th>
<td> <td>
<input type="text" name="email" size="25" maxlength="40" autocomplete="off"> <input type="text" name="email" size="25" maxlength="40" autocomplete="off">
{{ hiddenInputs([board.uri, id]) }} {{ antibot.html() }}
</td> </td>
</tr>{% endif %} </tr>{% endif %}
<tr> <tr>
<th> <th>
{% trans %}Subject{% endtrans %} {% trans %}Subject{% endtrans %}
{{ hiddenInputs([board.uri, id]) }} {{ antibot.html() }}
</th> </th>
<td> <td>
<input style="float:left;" type="text" name="subject" size="25" maxlength="100" autocomplete="off"> <input style="float:left;" type="text" name="subject" size="25" maxlength="100" autocomplete="off">
@ -39,22 +39,22 @@
<tr> <tr>
<th> <th>
{% trans %}Comment{% endtrans %} {% trans %}Comment{% endtrans %}
{{ hiddenInputs([board.uri, id]) }} {{ antibot.html() }}
</th> </th>
<td> <td>
<textarea name="body" id="body" rows="5" cols="35"></textarea> <textarea name="body" id="body" rows="5" cols="35"></textarea>
{{ hiddenInputs([board.uri, id]) }} {{ antibot.html() }}
</td> </td>
</tr> </tr>
{% if config.recaptcha %} {% if config.recaptcha %}
<tr> <tr>
<th> <th>
{% trans %}Verification{% endtrans %} {% trans %}Verification{% endtrans %}
{{ hiddenInputs([board.uri, id]) }} {{ antibot.html() }}
</th> </th>
<td> <td>
<script type="text/javascript" src="http://www.google.com/recaptcha/api/challenge?k={{ config.recaptcha_public }}"></script> <script type="text/javascript" src="http://www.google.com/recaptcha/api/challenge?k={{ config.recaptcha_public }}"></script>
{{ hiddenInputs([board.uri, id]) }} {{ antibot.html() }}
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
@ -64,18 +64,18 @@
</th> </th>
<td> <td>
<input type="file" name="file"> <input type="file" name="file">
{{ hiddenInputs([board.uri, id]) }} {{ antibot.html() }}
</td> </td>
</tr> </tr>
{% if config.enable_embedding %} {% if config.enable_embedding %}
<tr> <tr>
<th> <th>
{% trans %}Embed{% endtrans %} {% trans %}Embed{% endtrans %}
{{ hiddenInputs([board.uri, id]) }} {{ antibot.html() }}
</th> </th>
<td> <td>
<input type="text" name="embed" size="30" maxlength="120" autocomplete="off"> <input type="text" name="embed" size="30" maxlength="120" autocomplete="off">
{{ hiddenInputs([board.uri, id]) }} {{ antibot.html() }}
</td> </td>
</tr> </tr>
{% endif %} {% 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> {% if not config.field_disable_password or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri)) %}<tr>
<th> <th>
{% trans %}Password{% endtrans %} {% trans %}Password{% endtrans %}
{{ hiddenInputs([board.uri, id]) }} {{ antibot.html() }}
</th> </th>
<td> <td>
<input type="password" name="password" size="12" maxlength="18" autocomplete="off"> <input type="password" name="password" size="12" maxlength="18" autocomplete="off">
<span class="unimportant">{% trans %}(For file deletion.){% endtrans %}</span> <span class="unimportant">{% trans %}(For file deletion.){% endtrans %}</span>
{{ hiddenInputs([board.uri, id]) }} {{ antibot.html() }}
</td> </td>
</tr>{% endif %} </tr>{% endif %}
</table> </table>
{{ hiddenInputs([board.uri, id]) }} {{ antibot.html(true) }}
<input type="hidden" name="hash" value="{{ hiddenInputsHash([board.uri, id]) }}"> <input type="hidden" name="hash" value="{{ antibot.hash() }}">
{{ hiddenInputs([board.uri, id], true) }}
</form> </form>
<script type="text/javascript">{% raw %} <script type="text/javascript">{% raw %}