diff --git a/inc/captcha/captcha.php b/inc/captcha/captcha.php new file mode 100644 index 00000000..7c54fa3c --- /dev/null +++ b/inc/captcha/captcha.php @@ -0,0 +1,357 @@ +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 ""; + } + + echo "
"; + $a = &$this->content; + $inside = false; + } + + foreach ($a as &$v) { + $letter = $v[0]; + + unset ($v[0]); + + echo "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 "
"; + } + + if (!$inside) { + echo ""; + } + + } + + 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(); +?> diff --git a/inc/captcha/config.php b/inc/captcha/config.php new file mode 100644 index 00000000..568c06c5 --- /dev/null +++ b/inc/captcha/config.php @@ -0,0 +1,16 @@ + 'SET NAMES utf8')); + + +// Captcha expiration: +$expires_in = 120; // 120 seconds + +// Captcha dimensions: +$width = 250; +$height = 80; + +// Captcha length: +$length = 6; diff --git a/inc/captcha/dbschema.sql b/inc/captcha/dbschema.sql new file mode 100644 index 00000000..504ea10c --- /dev/null +++ b/inc/captcha/dbschema.sql @@ -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; diff --git a/inc/captcha/entrypoint.php b/inc/captcha/entrypoint.php new file mode 100644 index 00000000..3b468a2a --- /dev/null +++ b/inc/captcha/entrypoint.php @@ -0,0 +1,85 @@ +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; +} diff --git a/inc/captcha/readme.md b/inc/captcha/readme.md new file mode 100644 index 00000000..8b1f538a --- /dev/null +++ b/inc/captcha/readme.md @@ -0,0 +1,10 @@ +I integrated this from: https://github.com/ctrlcctrlv/infinity/commit/62a6dac022cb338f7b719d0c35a64ab3efc64658 + +First import the captcha/dbschema.sql in your database 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. diff --git a/inc/config.php b/inc/config.php index 48659031..bd7753c8 100644 --- a/inc/config.php +++ b/inc/config.php @@ -289,6 +289,8 @@ 'embed', 'recaptcha_challenge_field', 'recaptcha_response_field', + 'captcha_cookie', + 'captcha_text', 'spoiler', 'page', 'file_url', @@ -304,6 +306,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; @@ -1029,6 +1051,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/'; diff --git a/install.php b/install.php index 20a4b874..2a23fdb6 100644 --- a/install.php +++ b/install.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, @@ -615,9 +615,14 @@ if (file_exists($config['has_installed'])) { UNIQUE KEY `cookie` (`cookie`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; ') or error(db_error()); - - - + 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()); diff --git a/install.sql b/install.sql index 037ee832..ddef1839 100644 --- a/install.sql +++ b/install.sql @@ -368,6 +368,20 @@ CREATE TABLE IF NOT EXISTS `theme_settings` ( KEY `theme` (`theme`) ) 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 */; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; \ No newline at end of file diff --git a/js/captcha.js b/js/captcha.js new file mode 100644 index 00000000..018588b7 --- /dev/null +++ b/js/captcha.js @@ -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(""+ + ""+ + "
"); + + $("#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); }); + }); + }); +} \ No newline at end of file diff --git a/js/quick-reply.js b/js/quick-reply.js index 408de410..cd1b9e8a 100644 --- a/js/quick-reply.js +++ b/js/quick-reply.js @@ -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('\ diff --git a/post.php b/post.php index 025d06ac..cc27e377 100644 --- a/post.php +++ b/post.php @@ -384,7 +384,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'] . + ''); } + } +} if (!(($post['op'] && $_POST['post'] == $config['button_newtopic']) || (!$post['op'] && $_POST['post'] == $config['button_reply']))) diff --git a/templates/post_form.html b/templates/post_form.html index e5e00009..d19d1587 100644 --- a/templates/post_form.html +++ b/templates/post_form.html @@ -86,6 +86,27 @@ {% endif %} + {% if config.captcha.enabled %} + + + {% trans %}Verification{% endtrans %} + + + + + + {% elseif config.new_thread_capt %} + {% if not id %} + + + {% trans %}Verification{% endtrans %} + + + + + + {% endif %} + {% endif %} {% if config.user_flag %} {% trans %}Flag{% endtrans %}