Merge pull request #224 from ghost/patch-2

Implementing Czaks captcha
This commit is contained in:
Marcin Łabanowski 2017-07-23 17:57:59 +02:00 committed by GitHub
commit 39715e3595
12 changed files with 605 additions and 7 deletions

357
inc/captcha/captcha.php Normal file
View File

@ -0,0 +1,357 @@
<?php
class CzaksCaptcha {
var $content = array();
var $width, $height, $color, $charset, $style;
function __construct($text, $left, $top, $charset=false) {
if (!$charset) {
$charset = 'abcdefghijklmnopqrstuvwxyz';
}
$len = mb_strlen($text, 'utf-8');
$this->width = $left;
$this->height = $top;
$this->charset = preg_split('//u', $charset);
$this->style = "";
for ($i = 0; $i < $len; $i++) {
$this->content[] = array(mb_substr($text, $i, 1, 'utf-8'), "top" => $top / 2 - $top / 4,
"left" => $left/10 + 9*$left*$i/10/$len,
"position" => "absolute");
}
$this->color = "hsla(".rand(1,360).", 76%, 78%, 1)";
$this->add_junk();
$this->mutate_sizes();
$this->mutate_positions();
$this->mutate_transform();
$this->mutate_anchors();
$this->randomize();
$this->mutate_containers();
$this->mutate_margins();
$this->mutate_styles();
$this->randomize();
}
function mutate_sizes() {
foreach ($this->content as &$v) {
if (!isset ($v['font-size']))
$v['font-size'] = rand($this->height/3 - 4, $this->height/3 + 8);
}
}
function mutate_positions() {
foreach ($this->content as &$v) {
$v['top'] += rand(-10,10);
$v['left'] += rand(-10,10);
}
}
function mutate_transform() {
$fromto = array('6'=>'9', '9'=>'6', '8'=>'8', '0'=>'0',
'z'=>'z', 's'=>'s', 'n'=>'u', 'u'=>'n',
'a'=>'ɐ', 'e'=>'ə', 'p'=>'d', 'd'=>'p',
'A'=>'∀', 'E'=>'∃', 'H'=>'H', 'o'=>'o',
'O'=>'O');
foreach ($this->content as &$v) {
$basefrom = -20;
$baseto = 20;
if (isset($fromto[$v[0]]) && rand(0,1)) {
$v[0] = $fromto[$v[0]];
$basefrom = 160;
$baseto = 200;
}
$v['transform'] = 'rotate('.rand($basefrom,$baseto).'deg)';
$v['-ms-transform'] = 'rotate('.rand($basefrom,$baseto).'deg)';
$v['-webkit-transform'] = 'rotate('.rand($basefrom,$baseto).'deg)';
}
}
function randomize(&$a = false) {
if ($a === false) {
$a = &$this->content;
}
shuffle($a);
foreach ($a as &$v) {
$this->shuffle_assoc($v);
if (is_array ($v[0])) {
$this->randomize($v[0]);
}
}
}
function add_junk() {
$count = rand(200, 300);
while ($count--) {
$elem = array();
$elem['top'] = rand(0, $this->height);
$elem['left'] = rand(0, $this->width);
$elem['position'] = 'absolute';
$elem[0] = $this->charset[rand(0, count($this->charset)-1)];
switch($t = rand (0,9)) {
case 0:
$elem['display'] = 'none'; break;
case 1:
$elem['top'] = rand(-60, -90); break;
case 2:
$elem['left'] = rand(-40, -70); break;
case 3:
$elem['top'] = $this->height + rand(10, 60); break;
case 4:
$elem['left'] = $this->width + rand(10, 60); break;
case 5:
$elem['color'] = $this->color; break;
case 6:
$elem['visibility'] = 'hidden'; break;
case 7:
$elem['height'] = rand(0,2);
$elem['overflow'] = 'hidden'; break;
case 8:
$elem['width'] = rand(0,1);
$elem['overflow'] = 'hidden'; break;
case 9:
$elem['font-size'] = rand(2, 6); break;
}
$this->content[] = $elem;
}
}
function mutate_anchors() {
foreach ($this->content as &$elem) {
if (rand(0,1)) {
$elem['right'] = $this->width - $elem['left'] - (int)(0.5*$elem['font-size']);
unset($elem['left']);
}
if (rand(0,1)) {
$elem['bottom'] = $this->height - $elem['top'] - (int)(1.5*$elem['font-size']);
unset($elem['top']);
}
}
}
function mutate_containers() {
for ($i = 0; $i <= 80; $i++) {
$new = [];
$new['width'] = rand(0, $this->width*2);
$new['height'] = rand(0, $this->height*2);
$new['top'] = rand(-$this->height * 2, $this->height * 2);
$new['bottom'] = $this->height - ($new['top'] + $new['height']);
$new['left'] = rand(-$this->width * 2, $this->width * 2);
$new['right'] = $this->width - ($new['left'] + $new['width']);
$new['position'] = 'absolute';
$new[0] = [];
$cnt = rand(0,10);
for ($j = 0; $j < $cnt; $j++) {
$elem = array_pop($this->content);
if (!$elem) break;
if (isset($elem['top'])) $elem['top'] -= $new['top'];
if (isset($elem['bottom'])) $elem['bottom'] -= $new['bottom'];
if (isset($elem['left'])) $elem['left'] -= $new['left'];
if (isset($elem['right'])) $elem['right'] -= $new['right'];
$new[0][] = $elem;
}
if (rand (0,1)) unset($new['top']);
else unset($new['bottom']);
if (rand (0,1)) unset($new['left']);
else unset($new['right']);
$this->content[] = $new;
shuffle($this->content);
}
}
function mutate_margins(&$a = false) {
if ($a === false) {
$a = &$this->content;
}
foreach ($a as &$v) {
$ary = ['top', 'left', 'bottom', 'right'];
shuffle($ary);
$cnt = rand(0,4);
$ary = array_slice($ary, 0, $cnt);
foreach ($ary as $prop) {
$margin = rand(-1000, 1000);
$v['margin-'.$prop] = $margin;
if (isset($v[$prop])) {
$v[$prop] -= $margin;
}
}
if (is_array($v[0])) {
$this->mutate_margins($v[0]);
}
}
}
function mutate_styles(&$a = false) {
if ($a === false) {
$a = &$this->content;
}
foreach ($a as &$v) {
$content = $v[0];
unset($v[0]);
$styles = array_splice($v, 0, rand(0, 6));
$v[0] = $content;
$id_or_class = rand(0,1);
$param = $id_or_class ? "id" : "class";
$prefix = $id_or_class ? "#" : ".";
$genname = "zz-".base_convert(rand(1,999999999), 10, 36);
if ($styles || rand(0,1)) {
$this->style .= $prefix.$genname."{";
$this->style .= $this->rand_whitespace();
foreach ($styles as $k => $val) {
if (is_int($val)) {
$val = "".$val."px";
}
$this->style .= "$k:";
$this->style .= $this->rand_whitespace();
$this->style .= "$val;";
$this->style .= $this->rand_whitespace();
}
$this->style .= "}";
$this->style .= $this->rand_whitespace();
}
$v[$param] = $genname;
if (is_array($v[0])) {
$this->mutate_styles($v[0]);
}
}
}
function to_html(&$a = false) {
$inside = true;
if ($a === false) {
if ($this->style) {
echo "<style type='text/css'>";
echo $this->style;
echo "</style>";
}
echo "<div style='position: relative; width: ".$this->width."px; height: ".$this->height."px; overflow: hidden; background-color: ".$this->color."'>";
$a = &$this->content;
$inside = false;
}
foreach ($a as &$v) {
$letter = $v[0];
unset ($v[0]);
echo "<div";
echo $this->rand_whitespace(1);
if (isset ($v['id'])) {
echo "id='$v[id]'";
echo $this->rand_whitespace(1);
unset ($v['id']);
}
if (isset ($v['class'])) {
echo "class='$v[class]'";
echo $this->rand_whitespace(1);
unset ($v['class']);
}
echo "style='";
foreach ($v as $k => $val) {
if (is_int($val)) {
$val = "".$val."px";
}
echo "$k:";
echo $this->rand_whitespace();
echo "$val;";
echo $this->rand_whitespace();
}
echo "'>";
echo $this->rand_whitespace();
if (is_array ($letter)) {
$this->to_html($letter);
}
else {
echo $letter;
}
echo "</div>";
}
if (!$inside) {
echo "</div>";
}
}
function rand_whitespace($r = 0) {
switch (rand($r,4)) {
case 0:
return "";
case 1:
return "\n";
case 2:
return "\t";
case 3:
return " ";
case 4:
return " ";
}
}
function shuffle_assoc(&$array) {
$keys = array_keys($array);
shuffle($keys);
foreach($keys as $key) {
$new[$key] = $array[$key];
}
$array = $new;
return true;
}
}
//$charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789卐";
//(new CzaksCaptcha("hotwheels", 300, 80, $charset))->to_html();
?>

16
inc/captcha/config.php Normal file
View File

@ -0,0 +1,16 @@
<?php
// We are using a custom path here to connect to the database.
// Why? Performance reasons.
$pdo = new PDO("mysql:dbname=database_name;host=localhost", "database_user", "database_password", array(PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8'));
// Captcha expiration:
$expires_in = 120; // 120 seconds
// Captcha dimensions:
$width = 250;
$height = 80;
// Captcha length:
$length = 6;

9
inc/captcha/dbschema.sql Normal file
View File

@ -0,0 +1,9 @@
SET NAMES utf8;
CREATE TABLE `captchas` (
`cookie` VARCHAR(50),
`extra` VARCHAR(200),
`text` VARCHAR(255),
`created_at` INT(11),
PRIMARY KEY (cookie, extra)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;

View File

@ -0,0 +1,85 @@
<?php
$mode = @$_GET['mode'];
require_once("captcha.php");
function rand_string($length, $charset) {
$ret = "";
while ($length--) {
$ret .= mb_substr($charset, rand(0, mb_strlen($charset, 'utf-8')-1), 1, 'utf-8');
}
return $ret;
}
function cleanup ($pdo, $expires_in) {
$pdo->prepare("DELETE FROM `captchas` WHERE `created_at` < ?")->execute([time() - $expires_in]);
}
switch ($mode) {
// Request: GET entrypoint.php?mode=get&extra=1234567890
// Response: JSON: cookie => "generatedcookie", captchahtml => "captchahtml", expires_in => 120
case "get":
if (!isset ($_GET['extra'])) {
die();
}
header("Content-type: application/json");
$extra = $_GET['extra'];
require_once("config.php");
$text = rand_string($length, $extra);
$captcha = new CzaksCaptcha($text, $width, $height, $extra);
$cookie = rand_string(20, "abcdefghijklmnopqrstuvwxyz");
ob_start();
$captcha->to_html();
$html = ob_get_contents();
ob_end_clean();
$query = $pdo->prepare("INSERT INTO `captchas` (`cookie`, `extra`, `text`, `created_at`) VALUES (?, ?, ?, ?)");
$query->execute( [$cookie, $extra, $text, time()]);
echo json_encode(["cookie" => $cookie, "captchahtml" => $html, "expires_in" => $expires_in]);
break;
// Request: GET entrypoint.php?mode=check&cookie=generatedcookie&extra=1234567890&text=captcha
// Response: 0 OR 1
case "check":
if (!isset ($_GET['mode'])
|| !isset ($_GET['cookie'])
|| !isset ($_GET['extra'])
|| !isset ($_GET['text'])) {
die();
}
require_once("config.php");
cleanup($pdo, $expires_in);
$query = $pdo->prepare("SELECT * FROM `captchas` WHERE `cookie` = ? AND `extra` = ?");
$query->execute([$_GET['cookie'], $_GET['extra']]);
$ary = $query->fetchAll();
if (!$ary) {
echo "0";
}
else {
$query = $pdo->prepare("DELETE FROM `captchas` WHERE `cookie` = ? AND `extra` = ?");
$query->execute([$_GET['cookie'], $_GET['extra']]);
if ($ary[0]['text'] !== $_GET['text']) {
echo "0";
}
else {
echo "1";
}
}
break;
}

10
inc/captcha/readme.md Normal file
View File

@ -0,0 +1,10 @@
I integrated this from: https://github.com/ctrlcctrlv/infinity/commit/62a6dac022cb338f7b719d0c35a64ab3efc64658
<strike>First import the captcha/dbschema.sql in your database</strike> it is no longer required.
In inc/captcha/config.php change the database_name database_user database_password to your own settings.
Add js/captcha.js in your instance-config.php or config.php
Go to Line 305 in the /inc/config file and copy the settings in instance config, while changing the url to your website.
Go to the line beneath it if you only want to enable it when posting a new thread.

View File

@ -288,6 +288,8 @@
'embed',
'recaptcha_challenge_field',
'recaptcha_response_field',
'captcha_cookie',
'captcha_text',
'spoiler',
'page',
'file_url',
@ -303,6 +305,26 @@
// Public and private key pair from https://www.google.com/recaptcha/admin/create
$config['recaptcha_public'] = '6LcXTcUSAAAAAKBxyFWIt2SO8jwx4W7wcSMRoN3f';
$config['recaptcha_private'] = '6LcXTcUSAAAAAOGVbVdhmEM1_SyRF4xTKe8jbzf_';
// Enable Custom Captcha you need to change a couple of settings
//Read more at: /captcha/instructions.md
$config['captcha'] = array();
// Enable custom captcha provider
$config['captcha']['enabled'] = false;
//New thread captcha
//Require solving a captcha to post a thread.
//Default off.
$config['new_thread_capt'] = false;
// Custom captcha get provider path (if not working get the absolute path aka your url.)
$config['captcha']['provider_get'] = '../inc/captcha/entrypoint.php';
// Custom captcha check provider path
$config['captcha']['provider_check'] = '../inc/captcha/entrypoint.php';
// Custom captcha extra field (eg. charset)
$config['captcha']['extra'] = 'abcdefghijklmnopqrstuvwxyz';
// Ability to lock a board for normal users and still allow mods to post. Could also be useful for making an archive board
$config['board_locked'] = false;
@ -1019,6 +1041,7 @@
// $config['additional_javascript'][] = 'js/auto-reload.js';
// $config['additional_javascript'][] = 'js/post-hover.js';
// $config['additional_javascript'][] = 'js/style-select.js';
// $config['additional_javascript'][] = 'js/captcha.js';
// Where these script files are located on the web. Defaults to $config['root'].
// $config['additional_javascript_url'] = 'http://static.example.org/tinyboard-javascript-stuff/';

View File

@ -1,7 +1,7 @@
<?php
// Installation/upgrade file
define('VERSION', '5.1.3');
define('VERSION', '5.1.4');
require 'inc/functions.php';
@ -560,7 +560,7 @@ if (file_exists($config['has_installed'])) {
query('ALTER TABLE ``mods`` CHANGE `salt` `version` VARCHAR(64) NOT NULL;') or error(db_error());
case '5.0.1':
case '5.1.0':
query('CREATE TABLE ``pages`` (
query('CREATE TABLE IF NOT EXISTS ``pages`` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`board` varchar(255) DEFAULT NULL,
`name` varchar(255) NOT NULL,
@ -575,7 +575,7 @@ if (file_exists($config['has_installed'])) {
query(sprintf("ALTER TABLE ``posts_%s`` ADD `cycle` int(1) NOT NULL AFTER `locked`", $board['uri'])) or error(db_error());
}
case '5.1.2':
query('CREATE TABLE ``nntp_references`` (
query('CREATE TABLE IF NOT EXISTS ``nntp_references`` (
`board` varchar(60) NOT NULL,
`id` int(11) unsigned NOT NULL,
`message_id` varchar(255) CHARACTER SET ascii NOT NULL,
@ -587,7 +587,14 @@ if (file_exists($config['has_installed'])) {
UNIQUE KEY `u_board_id` (`board`, `id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
') or error(db_error());
case '5.1.3':
query('CREATE TABLE IF NOT EXISTS ``captchas`` (
`cookie` varchar(50),
`extra` varchar(200),
`text` varchar(255),
`created_at` int(11),
PRIMARY KEY (`cookie`,`extra`),
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;') or error(db_error());
case false:
// TODO: enhance Tinyboard -> vichan upgrade path.
query("CREATE TABLE IF NOT EXISTS ``search_queries`` ( `ip` varchar(39) NOT NULL, `time` int(11) NOT NULL, `query` text NOT NULL) ENGINE=MyISAM DEFAULT CHARSET=utf8;") or error(db_error());

View File

@ -303,7 +303,7 @@ CREATE TABLE IF NOT EXISTS `ban_appeals` (
-- Table structure for table `pages`
--
CREATE TABLE `pages` (
CREATE TABLE IF NOT EXISTS `pages` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`board` varchar(58) CHARACTER SET utf8 DEFAULT NULL,
`name` varchar(255) CHARACTER SET utf8 NOT NULL,
@ -320,7 +320,7 @@ CREATE TABLE `pages` (
-- Table structure for table `nntp_references`
--
CREATE TABLE `nntp_references` (
CREATE TABLE IF NOT EXISTS `nntp_references` (
`board` varchar(30) NOT NULL,
`id` int(11) unsigned NOT NULL,
`message_id` varchar(255) CHARACTER SET ascii NOT NULL,
@ -332,6 +332,20 @@ CREATE TABLE `nntp_references` (
UNIQUE KEY `u_board_id` (`board`, `id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
-- --------------------------------------------------------
--
-- Table structure for table `captchas`
--
CREATE TABLE IF NOT EXISTS `captchas` (
`cookie` VARCHAR(50),
`extra` VARCHAR(200),
`text` VARCHAR(255),
`created_at` INT(11),
PRIMARY KEY (`cookie`,`extra`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;

43
js/captcha.js Normal file
View File

@ -0,0 +1,43 @@
var tout;
function redo_events(provider, extra) {
$('.captcha .captcha_text, textarea[id="body"]').off("focus").one("focus", function() { actually_load_captcha(provider, extra); });
}
function actually_load_captcha(provider, extra) {
$('.captcha .captcha_text, textarea[id="body"]').off("focus");
if (tout !== undefined) {
clearTimeout(tout);
}
$.getJSON(provider, {mode: 'get', extra: extra}, function(json) {
$(".captcha .captcha_cookie").val(json.cookie);
$(".captcha .captcha_html").html(json.captchahtml);
setTimeout(function() {
redo_events(provider, extra);
}, json.expires_in * 1000);
});
}
function load_captcha(provider, extra) {
$(function() {
$(".captcha>td").html("<input class='captcha_text' type='text' name='captcha_text' size='32' maxlength='6' autocomplete='off'>"+
"<input class='captcha_cookie' name='captcha_cookie' type='hidden'>"+
"<div class='captcha_html'></div>");
$("#quick-reply .captcha .captcha_text").prop("placeholder", _("Verification"));
$(".captcha .captcha_html").on("click", function() { actually_load_captcha(provider, extra); });
$(document).on("ajax_after_post", function() { actually_load_captcha(provider, extra); });
redo_events(provider, extra);
$(window).on("quick-reply", function() {
redo_events(provider, extra);
$("#quick-reply .captcha .captcha_html").html($("form:not(#quick-reply) .captcha .captcha_html").html());
$("#quick-reply .captcha .captcha_cookie").val($("form:not(#quick-reply) .captcha .captcha_cookie").html());
$("#quick-reply .captcha .captcha_html").on("click", function() { actually_load_captcha(provider, extra); });
});
});
}

View File

@ -281,7 +281,7 @@
$postForm.find('textarea[name="body"]').removeAttr('id').removeAttr('cols').attr('placeholder', _('Comment'));
$postForm.find('textarea:not([name="body"]),input[type="hidden"]').removeAttr('id').appendTo($dummyStuff);
$postForm.find('textarea:not([name="body"]),input[type="hidden"]:not(.captcha_cookie)').removeAttr('id').appendTo($dummyStuff);
$postForm.find('br').remove();
$postForm.find('table').prepend('<tr><th colspan="2">\

View File

@ -393,7 +393,20 @@ if (isset($_POST['delete'])) {
if (!$resp->is_valid) {
error($config['error']['captcha']);
}
// Same, but now with our custom captcha provider
if (($config['captcha']['enabled']) || (($post['op']) && ($config['new_thread_capt'])) ) {
$resp = file_get_contents($config['captcha']['provider_check'] . "?" . http_build_query([
'mode' => 'check',
'text' => $_POST['captcha_text'],
'extra' => $config['captcha']['extra'],
'cookie' => $_POST['captcha_cookie']
]));
if ($resp !== '1') {
error($config['error']['captcha'] .
'<script>if (actually_load_captcha !== undefined) actually_load_captcha("'.$config['captcha']['provider_get'].'", "'.$config['captcha']['extra'].'");</script>');
}
}
}
if (!(($post['op'] && $_POST['post'] == $config['button_newtopic']) ||
(!$post['op'] && $_POST['post'] == $config['button_reply'])))

View File

@ -79,6 +79,27 @@
</td>
</tr>
{% endif %}
{% if config.captcha.enabled %}
<tr class='captcha'>
<th>
{% trans %}Verification{% endtrans %}
</th>
<td>
<script>load_captcha("{{ config.captcha.provider_get }}", "{{ config.captcha.extra }}");</script>
</td>
</tr>
{% elseif config.new_thread_capt %}
{% if not id %}
<tr class='captcha'>
<th>
{% trans %}Verification{% endtrans %}
</th>
<td>
<script>load_captcha("{{ config.captcha.provider_get }}", "{{ config.captcha.extra }}");</script>
</td>
</tr>
{% endif %}
{% endif %}
{% if config.user_flag %}
<tr>
<th>{% trans %}Flag{% endtrans %}</th>