From d2de4419bddbb7b0e09ced1f01d3ccb45aecc50f Mon Sep 17 00:00:00 2001 From: Matthieu Date: Thu, 14 Jan 2016 21:05:24 +0100 Subject: Added: config option to hide email in post. (prevent emailfag but let the possibility to sage and noko) --- inc/config.php | 3 +++ 1 file changed, 3 insertions(+) (limited to 'inc/config.php') diff --git a/inc/config.php b/inc/config.php index 69b4bd3a..d5bdbfe5 100644 --- a/inc/config.php +++ b/inc/config.php @@ -550,6 +550,9 @@ // When true, the sage won't be displayed $config['hide_sage'] = false; + // Don't display user's email when it's not "sage" + $config['hide_email'] = false; + // Attach country flags to posts. $config['country_flags'] = false; -- cgit v1.3 From caaf741691c9d44a9a186d6ce924fecb04f4f347 Mon Sep 17 00:00:00 2001 From: czaks Date: Fri, 22 Apr 2016 05:35:43 +0200 Subject: [SECURITY] keep up with modern password hashing standards --- inc/config.php | 15 ++++++++++++ inc/mod/auth.php | 71 ++++++++++++++++++++++++++++++++++++++++++++++--------- inc/mod/pages.php | 11 ++++----- install.php | 7 +++++- install.sql | 4 ++-- 5 files changed, 87 insertions(+), 21 deletions(-) (limited to 'inc/config.php') diff --git a/inc/config.php b/inc/config.php index d5bdbfe5..c0e967af 100644 --- a/inc/config.php +++ b/inc/config.php @@ -1672,3 +1672,18 @@ ''. ''. ''; + + // Password hashing function + // + // $5$ <- SHA256 + // $6$ <- SHA512 + // + // 25000 rounds make for ~0.05s on my 2015 Core i3 computer. + // + // https://secure.php.net/manual/en/function.crypt.php + $config['password_crypt'] = '$6$rounds=25000$'; + + // Password hashing method version + // If set to 0, it won't upgrade hashes using old password encryption schema, only create new. + // You can set it to a higher value, to further migrate to other password hashing function. + $config['password_crypt_version'] = 1; diff --git a/inc/mod/auth.php b/inc/mod/auth.php index 01ed5b68..fa1a0f4f 100644 --- a/inc/mod/auth.php +++ b/inc/mod/auth.php @@ -18,7 +18,18 @@ function mkhash($username, $password, $salt = false) { } // generate hash (method is not important as long as it's strong) - $hash = substr(base64_encode(md5($username . $config['cookies']['salt'] . sha1($username . $password . $salt . ($config['mod']['lock_ip'] ? $_SERVER['REMOTE_ADDR'] : ''), true), true)), 0, 20); + $hash = substr( + base64_encode( + md5( + $username . $config['cookies']['salt'] . sha1( + $username . $password . $salt . ( + $config['mod']['lock_ip'] ? $_SERVER['REMOTE_ADDR'] : '' + ), true + ) . sha1($config['password_crypt_version']) // Log out users being logged in with older password encryption schema + , true + ) + ), 0, 20 + ); if (isset($generated_salt)) return array($hash, $salt); @@ -26,25 +37,63 @@ function mkhash($username, $password, $salt = false) { return $hash; } -function generate_salt() { - mt_srand(microtime(true) * 100000 + memory_get_usage(true)); - return md5(uniqid(mt_rand(), true)); +function crypt_password_old($password) { + $salt = generate_salt(); + $password = hash('sha256', $salt . sha1($password)); + return array($salt, $password); } -function login($username, $password, $makehash=true) { - global $mod; - - // SHA1 password - if ($makehash) { - $password = sha1($password); +function crypt_password($password) { + global $config; + // `salt` database field is reused as a version value. We don't want it to be 0. + $version = $config['password_crypt_version'] ? $config['password_crypt_version'] : 1; + $new_salt = generate_salt(); + $password = crypt($password, $config['password_crypt'] . $new_salt . "$"); + return array($version, $password); +} + +function test_password($password, $salt, $test) { + global $config; + + // Version = 0 denotes an old password hashing schema. In the same column, the + // password hash was kept previously + $version = (strlen($salt) <= 8) ? (int) $salt : 0; + + if ($version == 0) { + $comp = hash('sha256', $salt . sha1($test)); } + else { + $comp = crypt($test, $password); + } + return array($version, hash_equals($password, $comp)); +} + +function generate_salt() { + // 128 bits of entropy + return strtr(base64_encode(mcrypt_create_iv(16, MCRYPT_DEV_URANDOM)), '+', '.'); +} + +function login($username, $password) { + global $mod, $config; $query = prepare("SELECT `id`, `type`, `boards`, `password`, `salt` FROM ``mods`` WHERE `username` = :username"); $query->bindValue(':username', $username); $query->execute() or error(db_error($query)); if ($user = $query->fetch(PDO::FETCH_ASSOC)) { - if ($user['password'] === hash('sha256', $user['salt'] . $password)) { + list($version, $ok) = test_password($user['password'], $user['salt'], $password); + + if ($ok) { + if ($config['password_crypt_version'] > $version) { + // It's time to upgrade the password hashing method! + list ($user['salt'], $user['password']) = crypt_password($password); + $query = prepare("UPDATE ``mods`` SET `password` = :password, `salt` = :salt WHERE `id` = :id"); + $query->bindValue(':password', $user['password']); + $query->bindValue(':salt', $user['salt']); + $query->bindValue(':id', $user['id']); + $query->execute() or error(db_error($query)); + } + return $mod = array( 'id' => $user['id'], 'type' => $user['type'], diff --git a/inc/mod/pages.php b/inc/mod/pages.php index 2c679b20..a07de4c7 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -1734,9 +1734,8 @@ function mod_user($uid) { } if ($_POST['password'] != '') { - $salt = generate_salt(); - $password = hash('sha256', $salt . sha1($_POST['password'])); - + list($salt, $password) = crypt_password($_POST['password']); + $query = prepare('UPDATE ``mods`` SET `password` = :password, `salt` = :salt WHERE `id` = :id'); $query->bindValue(':id', $uid); $query->bindValue(':password', $password); @@ -1761,8 +1760,7 @@ function mod_user($uid) { if (hasPermission($config['mod']['change_password']) && $uid == $mod['id'] && isset($_POST['password'])) { if ($_POST['password'] != '') { - $salt = generate_salt(); - $password = hash('sha256', $salt . sha1($_POST['password'])); + list($salt, $password) = crypt_password($_POST['password']); $query = prepare('UPDATE ``mods`` SET `password` = :password, `salt` = :salt WHERE `id` = :id'); $query->bindValue(':id', $uid); @@ -1834,8 +1832,7 @@ function mod_user_new() { if (!isset($config['mod']['groups'][$type]) || $type == DISABLED) error(sprintf($config['error']['invalidfield'], 'type')); - $salt = generate_salt(); - $password = hash('sha256', $salt . sha1($_POST['password'])); + list($salt, $password) = crypt_password($_POST['password']); $query = prepare('INSERT INTO ``mods`` VALUES (NULL, :username, :password, :salt, :type, :boards)'); $query->bindValue(':username', $_POST['username']); diff --git a/install.php b/install.php index 95caf8ec..5a2d724a 100644 --- a/install.php +++ b/install.php @@ -1,10 +1,12 @@ $config, @@ -551,6 +553,9 @@ if (file_exists($config['has_installed'])) { foreach ($boards as &$board) { query(sprintf('ALTER TABLE ``posts_%s`` ADD `slug` VARCHAR(255) DEFAULT NULL AFTER `embed`;', $board['uri'])) or error(db_error()); } + case '4.9.93': + query('ALTER TABLE ``mods`` CHANGE `password` `password` VARCHAR(255) NOT NULL;') or error(db_error()); + query('ALTER TABLE ``mods`` CHANGE `salt` `salt` VARCHAR(64) NOT NULL;') 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 969107a2..fbf220c1 100644 --- a/install.sql +++ b/install.sql @@ -131,8 +131,8 @@ CREATE TABLE IF NOT EXISTS `modlogs` ( CREATE TABLE IF NOT EXISTS `mods` ( `id` smallint(6) unsigned NOT NULL AUTO_INCREMENT, `username` varchar(30) NOT NULL, - `password` char(64) CHARACTER SET ascii NOT NULL COMMENT 'SHA256', - `salt` char(32) CHARACTER SET ascii NOT NULL, + `password` varchar(256) CHARACTER SET ascii NOT NULL COMMENT 'SHA256', + `salt` varchar(64) CHARACTER SET ascii NOT NULL, `type` smallint(2) NOT NULL, `boards` text CHARACTER SET utf8 NOT NULL, PRIMARY KEY (`id`), -- cgit v1.3 From dcf5d699bd6ce5770ebd515cc14219f3015dd63a Mon Sep 17 00:00:00 2001 From: czaks Date: Thu, 5 May 2016 08:21:21 +0200 Subject: simplify the md5 execution logic --- inc/api.php | 7 ++++++- inc/config.php | 4 ++-- post.php | 44 ++++++++++++++++++++++---------------------- 3 files changed, 30 insertions(+), 25 deletions(-) (limited to 'inc/config.php') diff --git a/inc/api.php b/inc/api.php index 57e4d367..9483200e 100644 --- a/inc/api.php +++ b/inc/api.php @@ -92,7 +92,12 @@ class Api { $dotPos = strrpos($file->file, '.'); $apiPost['ext'] = substr($file->file, $dotPos); $apiPost['tim'] = substr($file->file, 0, $dotPos); - $apiPost['md5'] = base64_encode(hex2bin($post->filehash)); + if (isset ($file->hash) && $post->filehash) { + $apiPost['md5'] = base64_encode(hex2bin($file->hash)); + } + else if (isset ($post->filehash) && $post->filehash) { + $apiPost['md5'] = base64_encode(hex2bin($post->filehash)); + } } private function translatePost($post, $threadsPage = false) { diff --git a/inc/config.php b/inc/config.php index c0e967af..b92c4ac0 100644 --- a/inc/config.php +++ b/inc/config.php @@ -807,8 +807,8 @@ // Set this to true if you're using a BSD $config['bsd_md5'] = false; - // Set this to true if you're having problems with image duplicated error and bsd_md5 doesn't help. - $config['php_md5'] = false; + // Set this to true if you're using Linux and you can execute `md5sum` binary. + $config['gnu_md5'] = false; // Number of posts in a "View Last X Posts" page $config['noko50_count'] = 50; diff --git a/post.php b/post.php index 729170a7..5c755a3c 100644 --- a/post.php +++ b/post.php @@ -572,7 +572,12 @@ if (isset($_POST['delete'])) { if ($post['has_file']) { - $fnarray = array(); + $md5cmd = false; + if ($config['bsd_md5']) $md5cmd = 'md5 -r'; + if ($config['gnu_md5']) $md5cmd = 'md5sum'; + + $allhashes = ''; + foreach ($post['files'] as $key => &$file) { if ($post['op'] && $config['allowed_ext_op']) { if (!in_array($file['extension'], $config['allowed_ext_op'])) @@ -586,34 +591,29 @@ if (isset($_POST['delete'])) { // Truncate filename if it is too long $file['filename'] = mb_substr($file['filename'], 0, $config['max_filename_len']); - if (!isset($filenames)) { - $filenames = escapeshellarg($file['tmp_name']); - } else { - $filenames .= (' ' . escapeshellarg($file['tmp_name'])); - } - - $fnarray[] = $file['tmp_name']; - $upload = $file['tmp_name']; if (!is_readable($upload)) error($config['error']['nomove']); + + if ($md5cmd) { + $output = shell_exec_error($md5cmd . " < " . escapeshellarg($upload)); + $output = explode(' ', $output); + $hash = $output[0]; + } + else { + $hash = md5_file($upload); + } + + $file['hash'] = $hash; + $allhashes .= $hash; } - - $md5cmd = $config['bsd_md5'] ? 'md5 -r' : 'md5sum'; - if (!$config['php_md5'] && $output = shell_exec_error("cat $filenames | $md5cmd")) { - $explodedvar = explode(' ', $output); - $hash = $explodedvar[0]; + if (count ($post['files']) == 1) { $post['filehash'] = $hash; - } elseif ($config['max_images'] === 1) { - $post['filehash'] = md5_file($upload); - } else { - $str_to_hash = ''; - foreach ($fnarray as $i => $f) { - $str_to_hash .= file_get_contents($f); - } - $post['filehash'] = md5($str_to_hash); + } + else { + $post['filehash'] = md5($allhashes); } } -- cgit v1.3 From 8943bb0bb378b629e0b1cf34f66094eb61f8453e Mon Sep 17 00:00:00 2001 From: Fredrick Brennan Date: Fri, 13 Mar 2015 12:45:57 +0800 Subject: Rewrite report system due to flooding --- inc/config.php | 3 +++ js/fix-report-delete-submit.js | 15 ++++----------- post.php | 20 +++++++++++++++++++- templates/report.html | 17 +++++++++++++++++ 4 files changed, 43 insertions(+), 12 deletions(-) create mode 100644 templates/report.html (limited to 'inc/config.php') diff --git a/inc/config.php b/inc/config.php index b92c4ac0..ad659343 100644 --- a/inc/config.php +++ b/inc/config.php @@ -1687,3 +1687,6 @@ // If set to 0, it won't upgrade hashes using old password encryption schema, only create new. // You can set it to a higher value, to further migrate to other password hashing function. $config['password_crypt_version'] = 1; + + // Use CAPTCHA for reports? + $config['report_captcha'] = false; diff --git a/js/fix-report-delete-submit.js b/js/fix-report-delete-submit.js index 71c97f74..0b1ff840 100644 --- a/js/fix-report-delete-submit.js +++ b/js/fix-report-delete-submit.js @@ -43,19 +43,12 @@ Menu.onclick(function(e, $buf) { var postId = $ele.find('.post_no').not('[id]').text(); $buf.find('#report_menu,#global_report_menu').click(function(e) { - $('#delete_'+postId).prop('checked', 'checked'); - if ($(this).attr('id') === 'global_report_menu') { - header = "

Attention!

This form is only for reporting child pornography, bot spam and credit card numbers, social security numbers or banking information. DMCA requests and all other deletion requests MUST be sent via email to admin@8chan.co.

8chan is unmoderated and allows posts without collecting ANY information from the poster less the details of their post. Furthermore, all boards on 8chan are user created and not actively monitored by anyone but the board creator.

8chan has a small volunteer staff to handle this queue, please do not waste their time by filling it with nonsense! If you made a report with this tool and the post was not deleted, do not make the report again! Email admin@8chan.co instead. Abuse of the global report system could lead to address blocks against your IP from 8chan.

Again, 8chan's global volunteers do not handle board specific issues. You most likely want to click \"Report\" instead to reach the creator and volunteers he assigned to this board.

"; - $('#global_report').prop('checked', 'checked'); + if ($(this).attr('id') === "global_report_menu") { + var global = '&global'; } else { - header = ""; - $('#global_report').prop('checked', ''); + var global = ''; } - alert(header+"Enter reason below...
", true, function(){ - $('#reason').val($('#alert_reason').val()); - $('input[name=report][type=submit]').click(); - }); - + window.open(configRoot+'report.php?board='+board_name+'&post=delete_'+postId+global, "", (global?"width=600, height=575":"width=500, height=275")); }); }); diff --git a/post.php b/post.php index 1c511f9c..3b4f3311 100644 --- a/post.php +++ b/post.php @@ -124,6 +124,23 @@ if (isset($_POST['delete'])) { if (count($report) > $config['report_limit']) error($config['error']['toomanyreports']); + + if ($config['report_captcha'] && !isset($_POST['captcha_text'], $_POST['captcha_cookie'])) { + error($config['error']['bot']); + } + + if ($config['report_captcha']) { + $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']); + } + } $reason = escape_markup_modifiers($_POST['reason']); markup($reason); @@ -153,7 +170,8 @@ if (isset($_POST['delete'])) { $root = $is_mod ? $config['root'] . $config['file_mod'] . '?/' : $config['root']; if (!isset($_POST['json_response'])) { - header('Location: ' . $root . $board['dir'] . $config['file_index'], true, $config['redirect_http']); + $index = $root . $board['dir'] . $config['file_index']; + echo Element('page.html', array('config' => $config, 'body' => '', 'title' => _('Report submitted!'))); } else { header('Content-Type: text/json'); echo json_encode(array('success' => true)); diff --git a/templates/report.html b/templates/report.html new file mode 100644 index 00000000..291f9313 --- /dev/null +++ b/templates/report.html @@ -0,0 +1,17 @@ +
+ + + {% if global %} + +

Attention!

This form is only for reporting child pornography, bot spam and credit card numbers, social security numbers or banking information. DMCA requests and all other deletion requests MUST be sent via email to admin@8chan.co.

8chan is unmoderated and allows posts without collecting ANY information from the poster less the details of their post. Furthermore, all boards on 8chan are user created and not actively monitored by anyone but the board creator.

8chan has a small volunteer staff to handle this queue, please do not waste their time by filling it with nonsense! If you made a report with this tool and the post was not deleted, do not make the report again! Email admin@8chan.co instead. Abuse of the global report system could lead to address blocks against your IP from 8chan.

Again, 8chan's global volunteers do not handle board specific issues. You most likely want to click "Report" instead to reach the creator and volunteers he assigned to this board.

+ {% endif %} +

{% trans %}Enter reason below...{% endtrans %}

+ + {% if config.report_captcha %} +

{% trans %}To submit your report, please fill out the CAPTCHA below.{% endtrans %}

+ {{ captcha['html'] }}
+ +
+ {% endif %} + + -- cgit v1.3 From 632d0a76d0afc5bd7a8cc90e6625a74fe432065a Mon Sep 17 00:00:00 2001 From: 8chan Date: Wed, 31 Dec 2014 22:37:19 -0800 Subject: Display placeholder if no file in catalog/theme.php; czaks: fix the code a bit --- inc/config.php | 2 +- templates/themes/catalog/theme.php | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'inc/config.php') diff --git a/inc/config.php b/inc/config.php index ad659343..21f7b5cc 100644 --- a/inc/config.php +++ b/inc/config.php @@ -766,7 +766,7 @@ // Location of thumbnail to use for spoiler images. $config['spoiler_image'] = 'static/spoiler.png'; // Location of thumbnail to use for deleted images. - // $config['image_deleted'] = 'static/deleted.png'; + $config['image_deleted'] = 'static/deleted.png'; // When a thumbnailed image is going to be the same (in dimension), just copy the entire file and use // that as a thumbnail instead of resizing/redrawing. diff --git a/templates/themes/catalog/theme.php b/templates/themes/catalog/theme.php index 34b28e48..c7a8ee95 100644 --- a/templates/themes/catalog/theme.php +++ b/templates/themes/catalog/theme.php @@ -88,6 +88,8 @@ $post['file'] = $config['uri_thumb'] . $files[0]->thumb; } } + } else { + $post['file'] = $config['root'] . $config['image_deleted']; } if (empty($post['image_count'])) $post['image_count'] = 0; -- cgit v1.3 From 33ef3f9b013f9848a39df6aaa2ed092a3478fd11 Mon Sep 17 00:00:00 2001 From: czaks Date: Fri, 6 May 2016 14:14:22 +0200 Subject: synchronize catalog_link --- inc/config.php | 4 ++-- templates/thread.html | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) (limited to 'inc/config.php') diff --git a/inc/config.php b/inc/config.php index 21f7b5cc..a3087162 100644 --- a/inc/config.php +++ b/inc/config.php @@ -931,8 +931,8 @@ // Show page navigation links at the top as well. $config['page_nav_top'] = false; - // Show "Catalog" link in page navigation. Use with the Catalog theme. - // $config['catalog_link'] = 'catalog.html'; + // Show "Catalog" link in page navigation. Use with the Catalog theme. Set to false to disable. + $config['catalog_link'] = 'catalog.html'; // Board categories. Only used in the "Categories" theme. // $config['categories'] = array( diff --git a/templates/thread.html b/templates/thread.html index 2d4ae262..3ddb5b1f 100644 --- a/templates/thread.html +++ b/templates/thread.html @@ -62,7 +62,9 @@ [{% trans %}Return{% endtrans %}] [{% trans %}Go to top{% endtrans %}] - [{% trans %}Catalog{% endtrans %}] + {% if config.catalog_link %} + [{% trans %}Catalog{% endtrans %}] + {% endif %} -- cgit v1.3 From 7911c374e86ef8fa69c4545867b9d9d3de12d5c6 Mon Sep 17 00:00:00 2001 From: 8chan Date: Wed, 25 Feb 2015 17:21:49 -0800 Subject: Public action logs commit (log.php) Note: In a previous commit, I began making inc/mod/auth.php more modular with the check_login() function. Including it does NOT check mod login by default anymore like it does on vichan. You have to call check_login(). I've finally included it in inc/functions.php. If you have any custom pages that use inc/mod/auth.php, just including functions.php is enough now. =================================== Also: backports 351375185e5 (early 404) --- inc/config.php | 22 +++++++++++++++++----- inc/functions.php | 22 ++++++++++++++++++++-- inc/mod/auth.php | 2 +- inc/mod/pages.php | 36 ++++++++++++++++++++++++++++++++++++ install.sql | 1 + log.php | 24 ++++++++++++++++++++++++ mod.php | 6 +++--- post.php | 5 +++-- templates/mod/log.html | 16 ++++++++++++++-- tools/inc/cli.php | 1 - 10 files changed, 119 insertions(+), 16 deletions(-) create mode 100644 log.php (limited to 'inc/config.php') diff --git a/inc/config.php b/inc/config.php index a3087162..b6bc3b69 100644 --- a/inc/config.php +++ b/inc/config.php @@ -511,6 +511,13 @@ // The timeout for the above, in seconds. $config['upload_by_url_timeout'] = 15; + // Enable early 404? With default settings, a thread would 404 if it was to leave page 3, if it had less + // than 3 replies. + $config['early_404'] = false; + + $config['early_404_page'] = 3; + $config['early_404_replies'] = 5; + // A wordfilter (sometimes referred to as just a "filter" or "censor") automatically scans users’ posts // as they are submitted and changes or censors particular words or phrases. @@ -1537,25 +1544,30 @@ /* * ==================== - * Public post search + * Public pages * ==================== */ + + // Public post search settings $config['search'] = array(); // Enable the search form $config['search']['enable'] = false; // Maximal number of queries per IP address per minutes - $config['search']['queries_per_minutes'] = Array(15, 2); + $config['search']['queries_per_minutes'] = Array(15, 2); // Global maximal number of queries per minutes - $config['search']['queries_per_minutes_all'] = Array(50, 2); + $config['search']['queries_per_minutes_all'] = Array(50, 2); // Limit of search results - $config['search']['search_limit'] = 100; + $config['search']['search_limit'] = 100; // Boards for searching - //$config['search']['boards'] = array('a', 'b', 'c', 'd', 'e'); + //$config['search']['boards'] = array('a', 'b', 'c', 'd', 'e'); + + // Enable public logs? 0: NO, 1: YES, 2: YES, but drop names + $config['public_logs'] = 0; /* * ==================== diff --git a/inc/functions.php b/inc/functions.php index 8f650828..378e40b3 100755 --- a/inc/functions.php +++ b/inc/functions.php @@ -18,6 +18,7 @@ require_once 'inc/template.php'; require_once 'inc/database.php'; require_once 'inc/events.php'; require_once 'inc/api.php'; +require_once 'inc/mod/auth.php'; require_once 'inc/polyfill.php'; if (!extension_loaded('gettext')) { @@ -524,7 +525,8 @@ function setupBoard($array) { $board = array( 'uri' => $array['uri'], 'title' => $array['title'], - 'subtitle' => $array['subtitle'] + 'subtitle' => $array['subtitle'], + #'indexed' => $array['indexed'], ); // older versions @@ -1270,7 +1272,7 @@ function deletePost($id, $error_if_doesnt_exist=true, $rebuild_after=true) { return true; } -function clean() { +function clean($pid = false) { global $board, $config; $offset = round($config['max_pages']*$config['threads_per_page']); @@ -1281,6 +1283,22 @@ function clean() { $query->execute() or error(db_error($query)); while ($post = $query->fetch(PDO::FETCH_ASSOC)) { deletePost($post['id'], false, false); + if ($pid) modLog("Automatically deleting thread #{$post['id']} due to new thread #{$pid}"); + } + + // Bump off threads with X replies earlier, spam prevention method + if ($config['early_404']) { + $offset = round($config['early_404_page']*$config['threads_per_page']); + $query = prepare(sprintf("SELECT `id` AS `thread_id`, (SELECT COUNT(`id`) FROM ``posts_%s`` WHERE `thread` = `thread_id`) AS `reply_count` FROM ``posts_%s`` WHERE `thread` IS NULL ORDER BY `sticky` DESC, `bump` DESC LIMIT :offset, 9001", $board['uri'], $board['uri'])); + $query->bindValue(':offset', $offset, PDO::PARAM_INT); + $query->execute() or error(db_error($query)); + + while ($post = $query->fetch(PDO::FETCH_ASSOC)) { + if ($post['reply_count'] < $config['early_404_replies']) { + deletePost($post['thread_id'], false, false); + if ($pid) modLog("Automatically deleting thread #{$post['thread_id']} due to new thread #{$pid} (early 404 is set, #{$post['thread_id']} had {$post['reply_count']} replies)"); + } + } } } diff --git a/inc/mod/auth.php b/inc/mod/auth.php index 009e93ad..42f34196 100644 --- a/inc/mod/auth.php +++ b/inc/mod/auth.php @@ -130,7 +130,7 @@ function destroyCookies() { function modLog($action, $_board=null) { global $mod, $board, $config; $query = prepare("INSERT INTO ``modlogs`` VALUES (:id, :ip, :board, :time, :text)"); - $query->bindValue(':id', $mod['id'], PDO::PARAM_INT); + $query->bindValue(':id', (isset($mod['id']) ? $mod['id'] : -1), PDO::PARAM_INT); $query->bindValue(':ip', $_SERVER['REMOTE_ADDR']); $query->bindValue(':time', time(), PDO::PARAM_INT); $query->bindValue(':text', $action); diff --git a/inc/mod/pages.php b/inc/mod/pages.php index a90fbbd6..ca12eaf1 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -698,6 +698,42 @@ function mod_user_log($username, $page_no = 1) { mod_page(_('Moderation log'), 'mod/log.html', array('logs' => $logs, 'count' => $count, 'username' => $username)); } +function mod_board_log($board, $page_no = 1, $hide_names = false, $public = false) { + global $config; + + if ($page_no < 1) + error($config['error']['404']); + + if (!hasPermission($config['mod']['mod_board_log'], $board) && !$public) + error($config['error']['noaccess']); + + $query = prepare("SELECT `username`, `mod`, `ip`, `board`, `time`, `text` FROM ``modlogs`` LEFT JOIN ``mods`` ON `mod` = ``mods``.`id` WHERE `board` = :board ORDER BY `time` DESC LIMIT :offset, :limit"); + $query->bindValue(':board', $board); + $query->bindValue(':limit', $config['mod']['modlog_page'], PDO::PARAM_INT); + $query->bindValue(':offset', ($page_no - 1) * $config['mod']['modlog_page'], PDO::PARAM_INT); + $query->execute() or error(db_error($query)); + $logs = $query->fetchAll(PDO::FETCH_ASSOC); + + if (empty($logs) && $page_no > 1) + error($config['error']['404']); + + if (!hasPermission($config['mod']['show_ip'])) { + // Supports ipv4 only! + foreach ($logs as $i => &$log) { + $log['text'] = preg_replace_callback('/(?:)?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?:<\/a>)?/', function($matches) { + return "xxxx";//less_ip($matches[1]); + }, $log['text']); + } + } + + $query = prepare("SELECT COUNT(*) FROM ``modlogs`` LEFT JOIN ``mods`` ON `mod` = ``mods``.`id` WHERE `board` = :board"); + $query->bindValue(':board', $board); + $query->execute() or error(db_error($query)); + $count = $query->fetchColumn(); + + mod_page(_('Board log'), 'mod/log.html', array('logs' => $logs, 'count' => $count, 'board' => $board, 'hide_names' => $hide_names, 'public' => $public)); +} + function mod_view_board($boardName, $page_no = 1) { global $config, $mod; diff --git a/install.sql b/install.sql index 7e661450..024cb349 100644 --- a/install.sql +++ b/install.sql @@ -65,6 +65,7 @@ CREATE TABLE IF NOT EXISTS `boards` ( `uri` varchar(58) CHARACTER SET utf8 NOT NULL, `title` tinytext NOT NULL, `subtitle` tinytext, + -- `indexed` boolean default true, PRIMARY KEY (`uri`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4; diff --git a/log.php b/log.php new file mode 100644 index 00000000..1a660c4c --- /dev/null +++ b/log.php @@ -0,0 +1,24 @@ + {% if log.username %} - {{ log.username|e }} + {% if hide_names %} + hidden + {% else %} + {% if not mod|hasPermission(config.mod.modlog) %} + {{ log.username|e }} + {% else %} + {{ log.username|e }} + {% endif %} + {% endif %} {% elseif log.mod == -1 %} system {% else %} @@ -44,7 +52,11 @@ {% if count > logs|count %}

{% for i in range(0, (count - 1) / config.mod.modlog_page) %} - [{{ i + 1 }}] + {% if public %} + [{{ i + 1 }}] + {% else %} + [{{ i + 1 }}] + {% endif %} {% endfor %}

{% endif %} diff --git a/tools/inc/cli.php b/tools/inc/cli.php index f3e8824f..95d51573 100644 --- a/tools/inc/cli.php +++ b/tools/inc/cli.php @@ -39,7 +39,6 @@ if(!getenv('TINYBOARD_PATH')) { putenv('TINYBOARD_PATH=' . getcwd()); require 'inc/functions.php'; -require 'inc/mod/auth.php'; $mod = Array( 'id' => -1, -- cgit v1.3 From 95b1e103cb9f349d7d82dca071636d5cde855ead Mon Sep 17 00:00:00 2001 From: Fredrick Brennan Date: Sun, 29 Mar 2015 09:18:14 +0800 Subject: Edit static pages commit --- .gitmodules | 6 ++ inc/config.php | 6 ++ inc/functions.php | 43 ++++++++++++ inc/mod/pages.php | 162 +++++++++++++++++++++++++++++++++++++++++++ install.sql | 20 +++++- mod.php | 18 +++-- templates/mod/edit_page.html | 29 ++++++++ templates/mod/pages.html | 34 +++++++++ tools/import_rules.php | 16 +++++ 9 files changed, 328 insertions(+), 6 deletions(-) create mode 100644 templates/mod/edit_page.html create mode 100644 templates/mod/pages.html create mode 100644 tools/import_rules.php (limited to 'inc/config.php') diff --git a/.gitmodules b/.gitmodules index 73f90e0a..df07fdf3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,9 @@ [submodule "js/wPaint"] path = js/wPaint url = https://github.com/vichan-devel/wPaint.git + branch = master + +[submodule "inc/lib/parsedown"] + path = inc/lib/parsedown + url = https://github.com/vichan-devel/parsedown + branch = master diff --git a/inc/config.php b/inc/config.php index b6bc3b69..3fdb8d2f 100644 --- a/inc/config.php +++ b/inc/config.php @@ -1498,6 +1498,9 @@ $config['mod']['ban_appeals'] = MOD; // View the recent posts page $config['mod']['recent'] = MOD; + // Create pages + $config['mod']['edit_pages'] = MOD; + $config['pages_max'] = 10; // Config editor permissions $config['mod']['config'] = array(); @@ -1702,3 +1705,6 @@ // Use CAPTCHA for reports? $config['report_captcha'] = false; + + // Allowed HTML tags in ?/edit_pages. + $config['allowed_html'] = 'a[href|title],p,br,li,ol,ul,strong,em,u,h2,b,i,tt,div,img[src|alt|title],hr'; diff --git a/inc/functions.php b/inc/functions.php index 378e40b3..7970d05a 100755 --- a/inc/functions.php +++ b/inc/functions.php @@ -20,6 +20,7 @@ require_once 'inc/events.php'; require_once 'inc/api.php'; require_once 'inc/mod/auth.php'; require_once 'inc/polyfill.php'; +//require_once 'inc/lib/parsedown/Parsedown.php'; // we don't need that right now, do we? if (!extension_loaded('gettext')) { require_once 'inc/lib/gettext/gettext.inc'; @@ -2739,3 +2740,45 @@ function link_for($post, $page50 = false, $foreignlink = false, $thread = false) return sprintf($tpl, $id, $slug); } + +function prettify_textarea($s){ + return str_replace("\t", ' ', str_replace("\n", ' ', htmlentities($s))); +} + +class HTMLPurifier_URIFilter_NoExternalImages extends HTMLPurifier_URIFilter { + public $name = 'NoExternalImages'; + public function filter(&$uri, $c, $context) { + global $config; + $ct = $context->get('CurrentToken'); + + if (!$ct || $ct->name !== 'img') return true; + + if (!isset($uri->host) && !isset($uri->scheme)) return true; + + if (!in_array($uri->scheme . '://' . $uri->host . '/', $config['allowed_offsite_urls'])) { + error('No off-site links in board announcement images.'); + } + + return true; + } +} + +function purify_html($s) { + global $config; + + $c = HTMLPurifier_Config::createDefault(); + $c->set('HTML.Allowed', $config['allowed_html']); + $uri = $c->getDefinition('URI'); + $uri->addFilter(new HTMLPurifier_URIFilter_NoExternalImages(), $c); + $purifier = new HTMLPurifier($c); + $clean_html = $purifier->purify($s); + return $clean_html; +} + +function markdown($s) { + $pd = new Parsedown(); + $pd->setMarkupEscaped(true); + $pd->setimagesEnabled(false); + + return $pd->text($s); +} diff --git a/inc/mod/pages.php b/inc/mod/pages.php index ca12eaf1..328380d8 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -2628,6 +2628,167 @@ function mod_theme_rebuild($theme_name) { )); } +// This needs to be done for `secure` CSRF prevention compatibility, otherwise the $board will be read in as the token if editing global pages. +function delete_page_base($page = '', $board = false) { + global $config, $mod; + + if (empty($board)) + $board = false; + + if (!$board && $mod['boards'][0] !== '*') + error($config['error']['noaccess']); + + if (!hasPermission($config['mod']['edit_pages'], $board)) + error($config['error']['noaccess']); + + if ($board !== FALSE && !openBoard($board)) + error($config['error']['noboard']); + + if ($board) { + $query = prepare('DELETE FROM ``pages`` WHERE `board` = :board AND `name` = :name'); + $query->bindValue(':board', ($board ? $board : NULL)); + } else { + $query = prepare('DELETE FROM ``pages`` WHERE `board` IS NULL AND `name` = :name'); + } + $query->bindValue(':name', $page); + $query->execute() or error(db_error($query)); + + header('Location: ?/edit_pages' . ($board ? ('/' . $board) : ''), true, $config['redirect_http']); +} + +function mod_delete_page($page = '') { + delete_page_base($page); +} + +function mod_delete_page_board($page = '', $board = false) { + delete_page_base($page, $board); +} + +function mod_edit_page($id) { + global $config, $mod, $board; + + $query = prepare('SELECT * FROM ``pages`` WHERE `id` = :id'); + $query->bindValue(':id', $id); + $query->execute() or error(db_error($query)); + $page = $query->fetch(); + + if (!$page) + error(_('Could not find the page you are trying to edit.')); + + if (!$page['board'] && $mod['boards'][0] !== '*') + error($config['error']['noaccess']); + + if (!hasPermission($config['mod']['edit_pages'], $page['board'])) + error($config['error']['noaccess']); + + if ($page['board'] && !openBoard($page['board'])) + error($config['error']['noboard']); + + if (isset($_POST['method'], $_POST['content'])) { + $content = $_POST['content']; + $method = $_POST['method']; + $page['type'] = $method; + + if (!in_array($method, array('markdown', 'html', 'infinity'))) + error(_('Unrecognized page markup method.')); + + switch ($method) { + case 'markdown': + $write = markdown($content); + break; + case 'html': + if (hasPermission($config['mod']['rawhtml'])) { + $write = $content; + } else { + $write = purify_html($content); + } + break; + case 'infinity': + $c = $content; + markup($content); + $write = $content; + $content = $c; + } + + if (!isset($write) or !$write) + error(_('Failed to mark up your input for some reason...')); + + $query = prepare('UPDATE ``pages`` SET `type` = :method, `content` = :content WHERE `id` = :id'); + $query->bindValue(':method', $method); + $query->bindValue(':content', $content); + $query->bindValue(':id', $id); + $query->execute() or error(db_error($query)); + + $fn = ($board['uri'] ? ($board['uri'] . '/') : '') . $page['name'] . '.html'; + $body = "
$write
"; + $html = Element('page.html', array('config' => $config, 'body' => $body, 'title' => utf8tohtml($page['title']))); + file_write($fn, $html); + } + + if (!isset($content)) { + $query = prepare('SELECT `content` FROM ``pages`` WHERE `id` = :id'); + $query->bindValue(':id', $id); + $query->execute() or error(db_error($query)); + $content = $query->fetchColumn(); + } + + mod_page(sprintf(_('Editing static page: %s'), $page['name']), 'mod/edit_page.html', array('page' => $page, 'token' => make_secure_link_token("edit_page/$id"), 'content' => prettify_textarea($content), 'board' => $board)); +} + +function mod_pages($board = false) { + global $config, $mod, $pdo; + + if (empty($board)) + $board = false; + + if (!$board && $mod['boards'][0] !== '*') + error($config['error']['noaccess']); + + if (!hasPermission($config['mod']['edit_pages'], $board)) + error($config['error']['noaccess']); + + if ($board !== FALSE && !openBoard($board)) + error($config['error']['noboard']); + + if ($board) { + $query = prepare('SELECT * FROM ``pages`` WHERE `board` = :board'); + $query->bindValue(':board', $board); + } else { + $query = query('SELECT * FROM ``pages`` WHERE `board` IS NULL'); + } + $query->execute() or error(db_error($query)); + $pages = $query->fetchAll(PDO::FETCH_ASSOC); + + if (isset($_POST['page'])) { + if ($board and sizeof($pages) > $config['pages_max']) + error(sprintf(_('Sorry, this site only allows %d pages per board.'), $config['pages_max'])); + + if (!preg_match('/^[a-z0-9]{1,255}$/', $_POST['page'])) + error(_('Page names must be < 255 chars and may only contain lowercase letters A-Z and digits 1-9.')); + + foreach ($pages as $i => $p) { + if ($_POST['page'] === $p['name']) + error(_('Refusing to create a new page with the same name as an existing one.')); + } + + $title = ($_POST['title'] ? $_POST['title'] : NULL); + + $query = prepare('INSERT INTO ``pages``(board, title, name) VALUES(:board, :title, :name)'); + $query->bindValue(':board', ($board ? $board : NULL)); + $query->bindValue(':title', $title); + $query->bindValue(':name', $_POST['page']); + $query->execute() or error(db_error($query)); + + $pages[] = array('id' => $pdo->lastInsertId(), 'name' => $_POST['page'], 'board' => $board, 'title' => $title); + } + + foreach ($pages as $i => &$p) { + $p['delete_token'] = make_secure_link_token('edit_pages/delete/' . $p['name'] . ($board ? ('/' . $board) : '')); + } + + mod_page(_('Pages'), 'mod/pages.html', array('pages' => $pages, 'token' => make_secure_link_token('edit_pages' . ($board ? ('/' . $board) : '')), 'board' => $board)); +} + function mod_debug_antispam() { global $pdo, $config; @@ -2744,3 +2905,4 @@ function mod_debug_apc() { mod_page(_('Debug: APC'), 'mod/debug/apc.html', array('cached_vars' => $cached_vars)); } + diff --git a/install.sql b/install.sql index 024cb349..720e4ed5 100644 --- a/install.sql +++ b/install.sql @@ -245,7 +245,7 @@ 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; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4; -- -------------------------------------------------------- @@ -297,6 +297,24 @@ CREATE TABLE IF NOT EXISTS `ban_appeals` ( KEY `ban_id` (`ban_id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1 ; +-- -------------------------------------------------------- + +-- +-- Table structure for table `pages` +-- + +CREATE TABLE `pages` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `board` varchar(255) DEFAULT NULL, + `name` varchar(255) NOT NULL, + `title` varchar(255) DEFAULT NULL, + `type` varchar(255) DEFAULT NULL, + `content` text, + PRIMARY KEY (`id`), + UNIQUE KEY `u_pages` (`name`,`board`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4; + +>>>>>>> 12fa8ec... Edit static pages commit /*!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 */; diff --git a/mod.php b/mod.php index a6ff00cf..fbe679ac 100644 --- a/mod.php +++ b/mod.php @@ -33,11 +33,19 @@ $pages = array( '/log' => 'log', // modlog '/log/(\d+)' => 'log', // modlog - '/log:([^/]+)' => 'user_log', // modlog - '/log:([^/]+)/(\d+)' => 'user_log', // modlog - '/news' => 'secure_POST news', // view news - '/news/(\d+)' => 'secure_POST news', // view news - '/news/delete/(\d+)' => 'secure news_delete', // delete from news + '/log:([^/:]+)' => 'user_log', // modlog + '/log:([^/:]+)/(\d+)' => 'user_log', // modlog + '/log:b:([^/]+)' => 'board_log', // modlog + '/log:b:([^/]+)/(\d+)' => 'board_log', // modlog + + '/edit_news' => 'secure_POST news', // view news + '/edit_news/(\d+)' => 'secure_POST news', // view news + '/edit_news/delete/(\d+)' => 'secure news_delete', // delete from news + + '/edit_pages(?:/?(\%b)?)' => 'secure_POST pages', + '/edit_page/(\d+)' => 'secure_POST edit_page', + '/edit_pages/delete/([a-z0-9]+)' => 'secure delete_page', + '/edit_pages/delete/([a-z0-9]+)/(\%b)' => 'secure delete_page_board', '/noticeboard' => 'secure_POST noticeboard', // view noticeboard '/noticeboard/(\d+)' => 'secure_POST noticeboard', // view noticeboard diff --git a/templates/mod/edit_page.html b/templates/mod/edit_page.html new file mode 100644 index 00000000..3d132767 --- /dev/null +++ b/templates/mod/edit_page.html @@ -0,0 +1,29 @@ +
+
+ + + + + + +
{% trans %}Markup method{% endtrans %} + {% set allowed_html = config.allowed_html %} + {% trans %}

"markdown" is provided by parsedown. Note: images disabled.

+

"html" allows the following tags:
{{ allowed_html }}

+

"infinity" is the same as what is used in posts.

+

This page will not convert between formats,
choose it once or do the conversion yourself!

{% endtrans %} +
+ +
{% trans %}Page content{% endtrans %} +
+ {% trans %}Page will appear at:{% endtrans %} + {% if board %} {{ config.domain }}/{{ board.uri }}/{{ page.name }}.html + {% else %} {{ config.site }}/{{ page.name }}.html + {% endif %}
+ +
+
diff --git a/templates/mod/pages.html b/templates/mod/pages.html new file mode 100644 index 00000000..c2395c02 --- /dev/null +++ b/templates/mod/pages.html @@ -0,0 +1,34 @@ + +
+

+{% if board %} +{% set page_max = config.pages_max %} +{% trans %}This page allows you to create static pages for your board. The limit is {{ page_max }} pages per board. You will still have to link to your pages somewhere in your board, for example in a sticky or in the board's announcement. To make links in the board's announcement, use <a> HTML tags.{% endtrans %} +{% else %} +{% trans %}This page allows you to create static pages for your imageboard.{% endtrans %} +{% endif %} +

{% trans %}Existing pages{% endtrans %}

+{% if pages %} +
+ + +{% for page in pages %} + +{% endfor %} +{% else %} +No pages yet! +{% endif %} +
{% trans %}URL{% endtrans %}{% trans %}Title{% endtrans %}{% trans %}Edit{% endtrans %}{% trans %}Delete{% endtrans %}
{{ page.name }}{{ page.title }}{% trans %}Edit{% endtrans %}{% trans %}Delete{% endtrans %}
+
+
+

{% trans %}Create a new page{% endtrans %}

+
+ + + + +
{% trans %}URL{% endtrans %}{% trans %}Title{% endtrans %}
+ +
+ +
diff --git a/tools/import_rules.php b/tools/import_rules.php new file mode 100644 index 00000000..f583f035 --- /dev/null +++ b/tools/import_rules.php @@ -0,0 +1,16 @@ + $b) { + $rules = @file_get_contents($b.'/rules.txt'); + if ($rules && !empty(trim($rules))) { + $query = prepare('INSERT INTO ``pages``(name, title, type, board, content) VALUES("rules", "Rules", "html", :board, :content)'); + $query->bindValue(':board', $b); + $query->bindValue(':content', $rules); + $query->execute() or error(db_error($query)); + } +} -- cgit v1.3 From 505adffcdca1c1141b7b46f862b7939f2f4acf45 Mon Sep 17 00:00:00 2001 From: Fredrick Brennan Date: Fri, 3 Apr 2015 14:56:28 +0800 Subject: Cyclical threads ♺ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- inc/api.php | 1 + inc/config.php | 5 +++++ inc/functions.php | 8 +++++++- inc/mod/pages.php | 22 ++++++++++++++++++++++ install.php | 7 ++++++- mod.php | 1 + post.php | 11 ++++++++++- templates/post/post_controls.html | 8 +++++++- templates/post_thread.html | 13 ++++++++++--- templates/posts.sql | 1 + 10 files changed, 70 insertions(+), 7 deletions(-) (limited to 'inc/config.php') diff --git a/inc/api.php b/inc/api.php index b2d8adec..b280c25b 100644 --- a/inc/api.php +++ b/inc/api.php @@ -32,6 +32,7 @@ class Api { 'images' => 'images', 'sticky' => 'sticky', 'locked' => 'locked', + 'cycle' => 'cyclical', 'bump' => 'last_modified', 'embed' => 'embed', ); diff --git a/inc/config.php b/inc/config.php index 3fdb8d2f..82fdc882 100644 --- a/inc/config.php +++ b/inc/config.php @@ -1245,6 +1245,8 @@ $config['mod']['link_bumpunlock'] = '[-Sage]'; $config['mod']['link_editpost'] = '[Edit]'; $config['mod']['link_move'] = '[Move]'; + $config['mod']['link_cycle'] = '[Cycle]'; + $config['mod']['link_uncycle'] = '[-Cycle]'; // Moderator capcodes. $config['capcode'] = ' ## %s'; @@ -1388,6 +1390,9 @@ $config['mod']['deletebyip_global'] = ADMIN; // Sticky a thread $config['mod']['sticky'] = MOD; + // Cycle a thread + $config['mod']['cycle'] = MOD; + $config['cycle_limit'] = &$config['reply_limit']; // Lock a thread $config['mod']['lock'] = MOD; // Post in a locked thread diff --git a/inc/functions.php b/inc/functions.php index e9692d56..1bd1f32f 100755 --- a/inc/functions.php +++ b/inc/functions.php @@ -1021,7 +1021,7 @@ function insertFloodPost(array $post) { function post(array $post) { global $pdo, $board; - $query = prepare(sprintf("INSERT INTO ``posts_%s`` VALUES ( NULL, :thread, :subject, :email, :name, :trip, :capcode, :body, :body_nomarkup, :time, :time, :files, :num_files, :filehash, :password, :ip, :sticky, :locked, 0, :embed, :slug)", $board['uri'])); + $query = prepare(sprintf("INSERT INTO ``posts_%s`` VALUES ( NULL, :thread, :subject, :email, :name, :trip, :capcode, :body, :body_nomarkup, :time, :time, :files, :num_files, :filehash, :password, :ip, :sticky, :locked, :cycle, 0, :embed, :slug)", $board['uri'])); // Basic stuff if (!empty($post['subject'])) { @@ -1061,6 +1061,12 @@ function post(array $post) { $query->bindValue(':locked', false, PDO::PARAM_INT); } + if ($post['op'] && $post['mod'] && isset($post['cycle']) && $post['cycle']) { + $query->bindValue(':cycle', true, PDO::PARAM_INT); + } else { + $query->bindValue(':cycle', false, PDO::PARAM_INT); + } + if ($post['mod'] && isset($post['capcode']) && $post['capcode']) { $query->bindValue(':capcode', $post['capcode'], PDO::PARAM_INT); } else { diff --git a/inc/mod/pages.php b/inc/mod/pages.php index 6d1e20b4..303fa3dd 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -1089,6 +1089,28 @@ function mod_sticky($board, $unsticky, $post) { header('Location: ?/' . sprintf($config['board_path'], $board) . $config['file_index'], true, $config['redirect_http']); } +function mod_cycle($board, $uncycle, $post) { + global $config; + + if (!openBoard($board)) + error($config['error']['noboard']); + + if (!hasPermission($config['mod']['cycle'], $board)) + error($config['error']['noaccess']); + + $query = prepare(sprintf('UPDATE ``posts_%s`` SET `cycle` = :cycle WHERE `id` = :id AND `thread` IS NULL', $board)); + $query->bindValue(':id', $post); + $query->bindValue(':cycle', $uncycle ? 0 : 1); + $query->execute() or error(db_error($query)); + if ($query->rowCount()) { + modLog(($uncycle ? 'Made not cyclical' : 'Made cyclical') . " thread #{$post}"); + buildThread($post); + buildIndex(); + } + + header('Location: ?/' . sprintf($config['board_path'], $board) . $config['file_index'], true, $config['redirect_http']); +} + function mod_bumplock($board, $unbumplock, $post) { global $config; diff --git a/install.php b/install.php index 5a6512ba..de4d3dd0 100644 --- a/install.php +++ b/install.php @@ -1,7 +1,7 @@ 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/mod.php b/mod.php index fbe679ac..0378d55c 100644 --- a/mod.php +++ b/mod.php @@ -82,6 +82,7 @@ $pages = array( '/(\%b)/deletebyip/(\d+)(/global)?' => 'secure deletebyip', // delete all posts by IP address '/(\%b)/(un)?lock/(\d+)' => 'secure lock', // lock thread '/(\%b)/(un)?sticky/(\d+)' => 'secure sticky', // sticky thread + '/(\%b)/(un)?cycle/(\d+)' => 'secure cycle', // cycle thread '/(\%b)/bump(un)?lock/(\d+)' => 'secure bumplock', // "bumplock" thread '/themes' => 'themes_list', // manage themes diff --git a/post.php b/post.php index 0d3292bb..98aefe05 100644 --- a/post.php +++ b/post.php @@ -265,7 +265,7 @@ if (isset($_POST['delete'])) { //Check if thread exists if (!$post['op']) { - $query = prepare(sprintf("SELECT `sticky`,`locked`,`sage`,`slug` FROM ``posts_%s`` WHERE `id` = :id AND `thread` IS NULL LIMIT 1", $board['uri'])); + $query = prepare(sprintf("SELECT `sticky`,`locked`,`cycle`,`sage`,`slug` FROM ``posts_%s`` WHERE `id` = :id AND `thread` IS NULL LIMIT 1", $board['uri'])); $query->bindValue(':id', $post['thread'], PDO::PARAM_INT); $query->execute() or error(db_error()); @@ -867,6 +867,15 @@ if (isset($_POST['delete'])) { $post['slug'] = slugify($post); insertFloodPost($post); + + // Handle cyclical threads + if (!$post['op'] && isset($thread['cycle']) && $thread['cycle']) { + // Query is a bit weird due to "This version of MariaDB doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'" (MariaDB Ver 15.1 Distrib 10.0.17-MariaDB, for Linux (x86_64)) + $query = prepare(sprintf('DELETE FROM ``posts_%s`` WHERE `thread` = :thread AND `id` NOT IN (SELECT `id` FROM (SELECT `id` FROM ``posts_%s`` WHERE `thread` = :thread ORDER BY `id` DESC LIMIT :limit) i)', $board['uri'], $board['uri'])); + $query->bindValue(':thread', $post['thread']); + $query->bindValue(':limit', $config['cycle_limit'], PDO::PARAM_INT); + $query->execute() or error(db_error($query)); + } if (isset($post['antispam_hash'])) { incrementSpamHash($post['antispam_hash']); diff --git a/templates/post/post_controls.html b/templates/post/post_controls.html index 99a8484c..60dc6fc4 100644 --- a/templates/post/post_controls.html +++ b/templates/post/post_controls.html @@ -41,7 +41,6 @@ {% endif %} {% endif %} - {% if mod|hasPermission(config.mod.move, board.uri) %} {% if not post.thread %} {{ config.mod.link_move }}  @@ -49,6 +48,13 @@ {{ config.mod.link_move }}  {% endif %} {% endif %} +{% if mod|hasPermission(config.mod.cycle, board.uri) %} + {% if post.cycle %} + {{ config.mod.link_uncycle }}  + {% else %} + {{ config.mod.link_cycle }}  + {% endif %} +{% endif %} {% if mod|hasPermission(config.mod.editpost, board.uri) %} {{ config.mod.link_editpost }}  {% endif %} diff --git a/templates/post_thread.html b/templates/post_thread.html index 72ca2998..125d5f78 100644 --- a/templates/post_thread.html +++ b/templates/post_thread.html @@ -19,25 +19,32 @@ {{ post.id }} {% if post.sticky %} {% if config.font_awesome %} - + {% else %} Sticky {% endif %} {% endif %} {% if post.locked %} {% if config.font_awesome %} - + {% else %} Locked {% endif %} {% endif %} {% if post.bumplocked and (config.mod.view_bumplock < 0 or (post.mod and post.mod|hasPermission(config.mod.view_bumplock, board.uri))) %} {% if config.font_awesome %} - + {% else %} Bumplocked {% endif %} {% endif %} + {% if post.cycle %} + {% if config.font_awesome %} + + {% else %} + Cyclical + {% endif %} + {% endif %} {% if index %} [{% trans %}Reply{% endtrans %}] {% endif %} diff --git a/templates/posts.sql b/templates/posts.sql index 6b2249ef..070e687b 100644 --- a/templates/posts.sql +++ b/templates/posts.sql @@ -17,6 +17,7 @@ CREATE TABLE IF NOT EXISTS ``posts_{{ board }}`` ( `ip` varchar(39) CHARACTER SET ascii NOT NULL, `sticky` int(1) NOT NULL, `locked` int(1) NOT NULL, + `cycle` int(1) NOT NULL, `sage` int(1) NOT NULL, `embed` text, `slug` varchar(256) DEFAULT NULL, -- cgit v1.3 From b6f0317bde1b6d9a9ac5d81a660865a7aa9ad13d Mon Sep 17 00:00:00 2001 From: czaks Date: Sun, 8 May 2016 10:54:30 +0200 Subject: advanced build (1/2): a small refactor of index generating procedure; generation strategies --- inc/config.php | 70 ++++++++++++++++++++++--- inc/functions.php | 102 +++++++++++++++++++++++++++---------- inc/route.php | 5 +- smart_build.php | 9 +++- templates/themes/catalog/theme.php | 13 +++-- templates/themes/recent/theme.php | 5 +- templates/themes/sitemap/theme.php | 6 ++- templates/themes/ukko/theme.php | 6 ++- 8 files changed, 169 insertions(+), 47 deletions(-) (limited to 'inc/config.php') diff --git a/inc/config.php b/inc/config.php index 82fdc882..91804429 100644 --- a/inc/config.php +++ b/inc/config.php @@ -1203,16 +1203,74 @@ // Try not to build pages when we shouldn't have to. $config['try_smarter'] = true; - // EXPERIMENTAL: Defer static HTML building to a moment, when a given file is actually accessed. - // Warning: This option won't run out of the box. You need to tell your webserver, that a file - // for serving 403 and 404 pages is /smart_build.php. Also, you need to turn off indexes. +/* + * ==================== + * Advanced build + * ==================== + */ + + // Strategies for file generation. Also known as an "advanced build". If you don't have performance + // issues, you can safely ignore that part, because it's hard to configure and won't even work on + // your free webhosting. + // + // A strategy is a function, that given the PHP environment and ($fun, $array) variable pair, returns + // an $action array or false. + // + // $fun - a controller function name, see inc/controller.php. This is named after functions, so that + // we can generate the files in daemon. + // + // $array - arguments to be passed + // + // $action - action to be taken. It's an array, and the first element of it is one of the following: + // * "immediate" - generate the page immediately + // * "defer" - defer page generation to a moment a worker daemon gets to build it (serving a stale + // page in the meantime). The remaining arguments are daemon-specific. Daemon isn't + // implemented yet :DDDD inb4 while(true) { generate(Queue::Get()) }; (which is probably it). + // * "build_on_load" - defer page generation to a moment, when the user actually accesses the page. + // This is a smart_build behaviour. You shouldn't use this one too much, if you + // use it for active boards, the server may choke due to a possible race condition. + // See my blog post: https://engine.vichan.net/blog/res/2.html + // + // So, let's assume we want to build a thread 1324 on board /b/, because a new post appeared there. + // We try the first strategy, giving it arguments: 'sb_thread', array('b', 1324). The strategy will + // now return a value $action, denoting an action to do. If $action is false, we try another strategy. + // + // As I said, configuration isn't easy. + // + // 1. chmod 0777 directories: tmp/locks/ and tmp/queue/. + // 2. serve 403 and 404 requests to go thru smart_build.php + // for nginx, this blog post contains config snippets: https://engine.vichan.net/blog/res/2.html + // 3. disable indexes in your webserver + // 4. launch any number of daemons (eg. twice your number of threads?) using the command: + // $ tools/worker.php + // You don't need to do that step if you are not going to use the "defer" option. + // 5. enable smart_build_helper (see below) + // 6. edit the strategies (see inc/functions.php for the builtin ones). You can use lambdas. I will test + // various ones and include one that works best for me. + $config['generation_strategies'] = array(); + // Add a sane strategy. It forces to immediately generate a page user is about to land on. Otherwise, + // it has no opinion, so it needs a fallback strategy. + $config['generation_strategies'][] = 'strategy_sane'; + // Add an immediate catch-all strategy. This is the default function of imageboards: generate all pages + // on post time. + $config['generation_strategies'][] = 'strategy_immediate'; + // NOT RECOMMENDED: Instead of an all-"immediate" strategy, you can use an all-"build_on_load" one (used + // to be initialized using $config['smart_build']; ) for all pages instead of those to be build + // immediately. A rebuild done in this mode should remove all your static files + // $config['generation_strategies'][1] = 'strategy_smart_build'; + + // Deprecated. Leave it false. See above. $config['smart_build'] = false; - // Smart build related: when a file doesn't exist, where should we redirect? + // Use smart_build.php for dispatching missing requests. It may be useful without smart_build or advanced + // build, for example it will regenerate the missing files. + $config['smart_build_helper'] = true; + + // smart_build.php: when a file doesn't exist, where should we redirect? $config['page_404'] = '/404.html'; - // Smart build related: extra entrypoints. - $config['smart_build_entrypoints'] = array(); + // Extra controller entrypoints. Controller is used only by smart_build and advanced build. + $config['controller_entrypoints'] = array(); /* * ==================== diff --git a/inc/functions.php b/inc/functions.php index 1bd1f32f..2957c680 100755 --- a/inc/functions.php +++ b/inc/functions.php @@ -1319,7 +1319,8 @@ function thread_find_page($thread) { return floor(($config['threads_per_page'] + $index) / $config['threads_per_page']); } -function index($page, $mod=false) { +// $brief means that we won't need to generate anything yet +function index($page, $mod=false, $brief = false) { global $board, $config, $debug; $body = ''; @@ -1350,6 +1351,7 @@ function index($page, $mod=false) { unset($cached); } } + if (!isset($cached)) { $posts = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE `thread` = :id ORDER BY `id` DESC LIMIT :limit", $board['uri'])); $posts->bindValue(':id', $th['id']); @@ -1389,7 +1391,10 @@ function index($page, $mod=false) { } $threads[] = $thread; - $body .= $thread->build(true); + + if (!$brief) { + $body .= $thread->build(true); + } } if ($config['file_board']) { @@ -1610,27 +1615,28 @@ function checkMute() { function buildIndex($global_api = "yes") { global $board, $config, $build_pages; - if (!$config['smart_build']) { - $pages = getPages(); - if (!$config['try_smarter']) - $antibot = create_antibot($board['uri']); + $catalog_api_action = generation_strategy('sb_api', array($board['uri'])); - if ($config['api']['enabled']) { - $api = new Api(); - $catalog = array(); - } + $pages = null; + $antibot = null; + + if ($config['api']['enabled']) { + $api = new Api(); + $catalog = array(); } for ($page = 1; $page <= $config['max_pages']; $page++) { $filename = $board['dir'] . ($page == 1 ? $config['file_index'] : sprintf($config['file_page'], $page)); $jsonFilename = $board['dir'] . ($page - 1) . '.json'; // pages should start from 0 - if ((!$config['api']['enabled'] || $global_api == "skip" || $config['smart_build']) && $config['try_smarter'] - && isset($build_pages) && !empty($build_pages) && !in_array($page, $build_pages) ) + $wont_build_this_page = $config['try_smarter'] && isset($build_pages) && !empty($build_pages) && !in_array($page, $build_pages); + + if ((!$config['api']['enabled'] || $global_api == "skip") && $wont_build_this_page) continue; - if (!$config['smart_build']) { - $content = index($page); + $action = generation_strategy('sb_board', array($board['uri'], $page)); + if ($action == 'rebuild' || $catalog_api_action == 'rebuild') { + $content = index($page, false, $wont_build_this_page); if (!$content) break; @@ -1641,17 +1647,21 @@ function buildIndex($global_api = "yes") { file_write($jsonFilename, $json); $catalog[$page-1] = $threads; - } - if ($config['api']['enabled'] && $global_api != "skip" && $config['try_smarter'] && isset($build_pages) - && !empty($build_pages) && !in_array($page, $build_pages) ) - continue; + if ($wont_build_this_page) continue; + } if ($config['try_smarter']) { $antibot = create_antibot($board['uri'], 0 - $page); $content['current_page'] = $page; } + elseif (!$antibot) { + create_antibot($board['uri']); + } $antibot->reset(); + if (!$pages) { + $pages = getPages(); + } $content['pages'] = $pages; $content['pages'][$page-1]['selected'] = true; $content['btn'] = getPageButtons($content['pages']); @@ -1659,13 +1669,14 @@ function buildIndex($global_api = "yes") { file_write($filename, Element('index.html', $content)); } - else { + elseif ($action == 'delete' || $catalog_api_action == 'delete') { file_unlink($filename); file_unlink($jsonFilename); } } - if (!$config['smart_build'] && $page < $config['max_pages']) { + // $action is an action for our last page + if (($catalog_api_action == 'rebuild' || $action == 'rebuild' || $action == 'delete') && $page < $config['max_pages']) { for (;$page<=$config['max_pages'];$page++) { $filename = $board['dir'] . ($page==1 ? $config['file_index'] : sprintf($config['file_page'], $page)); file_unlink($filename); @@ -1679,13 +1690,13 @@ function buildIndex($global_api = "yes") { // json api catalog if ($config['api']['enabled'] && $global_api != "skip") { - if ($config['smart_build']) { + if ($catalog_api_action == 'delete') { $jsonFilename = $board['dir'] . 'catalog.json'; file_unlink($jsonFilename); $jsonFilename = $board['dir'] . 'threads.json'; file_unlink($jsonFilename); } - else { + elseif ($catalog_api_action == 'rebuild') { $json = json_encode($api->translateCatalog($catalog)); $jsonFilename = $board['dir'] . 'catalog.json'; file_write($jsonFilename, $json); @@ -2204,7 +2215,9 @@ function buildThread($id, $return = false, $mod = false) { if ($config['try_smarter'] && !$mod) $build_pages[] = thread_find_page($id); - if (!$config['smart_build'] || $return || $mod) { + $action = generation_strategy('sb_thread', array($board['uri'], $id)); + + if ($action == 'rebuild' || $return || $mod) { $query = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE (`thread` IS NULL AND `id` = :id) OR `thread` = :id ORDER BY `thread`,`id`", $board['uri'])); $query->bindValue(':id', $id, PDO::PARAM_INT); $query->execute() or error(db_error($query)); @@ -2239,26 +2252,26 @@ function buildThread($id, $return = false, $mod = false) { )); // json api - if ($config['api']['enabled']) { + if ($config['api']['enabled'] && !$mod) { $api = new Api(); $json = json_encode($api->translateThread($thread)); $jsonFilename = $board['dir'] . $config['dir']['res'] . $id . '.json'; file_write($jsonFilename, $json); } } - else { + elseif($action == 'delete') { $jsonFilename = $board['dir'] . $config['dir']['res'] . $id . '.json'; file_unlink($jsonFilename); } - if ($config['smart_build'] && !$return && !$mod) { + if ($action == 'delete' && !$return && !$mod) { $noko50fn = $board['dir'] . $config['dir']['res'] . link_for(array('id' => $id), true); file_unlink($noko50fn); file_unlink($board['dir'] . $config['dir']['res'] . link_for(array('id' => $id))); - } else if ($return) { + } elseif ($return) { return $body; - } else { + } elseif ($action == 'rebuild') { $noko50fn = $board['dir'] . $config['dir']['res'] . link_for($thread, true); if ($hasnoko50 || file_exists($noko50fn)) { buildThread50($id, $return, $mod, $thread, $antibot); @@ -2788,3 +2801,36 @@ function markdown($s) { return $pd->text($s); } + +function generation_strategy($fun, $array=array()) { global $config; + $action = false; + + foreach ($config['generation_strategies'] as $s) { + if ($strategy = $s($fun, $array)) { + break; + } + } + + switch ($strategy[0]) { + case 'immediate': + return 'rebuild'; + case 'defer': + // Ok, it gets interesting here :) + Queue::add(serialize(array('build', $fun, $array))); + return 'ignore'; + case 'build_on_load': + return 'delete'; + } +} + +function strategy_immediate($fun, $array) { + return array('immediate'); +} + +function strategy_smart_build($fun, $array) { + return array('build_on_load'); +} + +function strategy_sane($fun, $array) { global $config; + return false; +} diff --git a/inc/route.php b/inc/route.php index 66602d77..2a5c1732 100644 --- a/inc/route.php +++ b/inc/route.php @@ -7,7 +7,7 @@ defined('TINYBOARD') or exit; -function route($path) { +function route($path) { global $config; $entrypoints = array(); $entrypoints['/%b/'] = 'sb_board'; @@ -33,8 +33,11 @@ function route($path) { $entrypoints['/*/index.html'] = 'sb_ukko'; $entrypoints['/recent.html'] = 'sb_recent'; $entrypoints['/%b/catalog.html'] = 'sb_catalog'; + $entrypoints['/%b/index.rss'] = 'sb_catalog'; $entrypoints['/sitemap.xml'] = 'sb_sitemap'; + $entrypoints = array_merge($entrypoints, $config['controller_entrypoints']); + $reached = false; list($request) = explode('?', $path); diff --git a/smart_build.php b/smart_build.php index 58596055..7ca5fcbf 100644 --- a/smart_build.php +++ b/smart_build.php @@ -3,14 +3,16 @@ require_once("inc/functions.php"); require_once("inc/route.php"); require_once("inc/controller.php"); -if (!$config['smart_build'] && !$config["smart_build_helper"]) { - die('You need to enable $config["smart_build"] or $config["smart_build_helper"]'); +if (!$config["smart_build_helper"]) { + die('You need to enable $config["smart_build_helper"]'); } $config['smart_build'] = false; // Let's disable it, so we can build the page for real +$config['generation_strategies'] = array('strategy_immediate'); function after_open_board() { global $config; $config['smart_build'] = false; + $config['generation_strategies'] = array('strategy_immediate'); }; $request = $_SERVER['REQUEST_URI']; @@ -59,6 +61,9 @@ if ($reached) { elseif (preg_match('/\.xml$/', $request)) { header("Content-Type", "application/xml"); } + elseif (preg_match('/\.rss$/', $request)) { + header("Content-Type", "application/rss+xml"); + } else { header("Content-Type", "text/html; charset=utf-8"); } diff --git a/templates/themes/catalog/theme.php b/templates/themes/catalog/theme.php index 239d4dff..4f512c03 100644 --- a/templates/themes/catalog/theme.php +++ b/templates/themes/catalog/theme.php @@ -16,20 +16,25 @@ if ($action == 'all') { foreach ($boards as $board) { $b = new Catalog(); - if ($config['smart_build']) { + + $action = generation_strategy("sb_catalog", array($board)); + if ($action == 'delete') { file_unlink($config['dir']['home'] . $board . '/catalog.html'); + file_unlink($config['dir']['home'] . $board . '/index.rss'); } - else { + elseif ($action == 'rebuild') { $b->build($settings, $board); } } } elseif ($action == 'post-thread' || ($settings['update_on_posts'] && $action == 'post') || ($settings['update_on_posts'] && $action == 'post-delete') && in_array($board, $boards)) { $b = new Catalog(); - if ($config['smart_build']) { + $action = generation_strategy("sb_catalog", array($board)); + if ($action == 'delete') { file_unlink($config['dir']['home'] . $board . '/catalog.html'); + file_unlink($config['dir']['home'] . $board . '/index.rss'); } - else { + elseif ($action == 'rebuild') { $b->build($settings, $board); } } diff --git a/templates/themes/recent/theme.php b/templates/themes/recent/theme.php index f44e2529..95921c35 100644 --- a/templates/themes/recent/theme.php +++ b/templates/themes/recent/theme.php @@ -25,10 +25,11 @@ $this->excluded = explode(' ', $settings['exclude']); if ($action == 'all' || $action == 'post' || $action == 'post-thread' || $action == 'post-delete') { - if ($config['smart_build']) { + $action = generation_strategy('sb_recent', array()); + if ($action == 'delete') { file_unlink($config['dir']['home'] . $settings['html']); } - else { + elseif ($action == 'rebuild') { file_write($config['dir']['home'] . $settings['html'], $this->homepage($settings)); } } diff --git a/templates/themes/sitemap/theme.php b/templates/themes/sitemap/theme.php index 52779d53..6bc035bb 100644 --- a/templates/themes/sitemap/theme.php +++ b/templates/themes/sitemap/theme.php @@ -23,10 +23,12 @@ } } - if ($config['smart_build']) { + $action = generation_strategy('sb_sitemap', array()); + + if ($action == 'delete') { file_unlink($settings['path']); } - else { + elseif ($action == 'rebuild') { $boards = explode(' ', $settings['boards']); $threads = array(); diff --git a/templates/themes/ukko/theme.php b/templates/themes/ukko/theme.php index d6fc303c..e572c467 100644 --- a/templates/themes/ukko/theme.php +++ b/templates/themes/ukko/theme.php @@ -11,10 +11,12 @@ return; } - if ($config['smart_build']) { + $action = generation_strategy('sb_ukko', array()); + + if ($action == 'delete') { file_unlink($settings['uri'] . '/index.html'); } - else { + elseif ($action == 'rebuild') { file_write($settings['uri'] . '/index.html', $ukko->build()); } } -- cgit v1.3 From 12e6aba5d4a2965591b3df76e4f6c19e35475700 Mon Sep 17 00:00:00 2001 From: czaks Date: Sun, 8 May 2016 13:20:00 +0200 Subject: (2/2) advanced build. implement a daemon that will build static pages. implement a queue and a lock. fix notice in bans. and it even works! the daemon is basic right now, it could work in a mode that it will defer building certain pages until a certain time. --- inc/bans.php | 2 +- inc/config.php | 9 ++++++++- inc/functions.php | 40 +++++++++++++++++++++++++++++++++++----- smart_build.php | 2 +- tmp/queue/generate/.gitkeep | 0 tools/worker.php | 31 +++++++++++++++++++++++++++++++ 6 files changed, 76 insertions(+), 8 deletions(-) create mode 100644 tmp/queue/generate/.gitkeep create mode 100755 tools/worker.php (limited to 'inc/config.php') diff --git a/inc/bans.php b/inc/bans.php index 87e06e28..c468eb64 100644 --- a/inc/bans.php +++ b/inc/bans.php @@ -166,7 +166,7 @@ class Bans { if ($ban['post']) { $post = json_decode($ban['post']); - $ban['message'] = $post->body; + $ban['message'] = isset($post->body) ? $post->body : 0; } unset($ban['ipstart'], $ban['ipend'], $ban['post'], $ban['creator']); diff --git a/inc/config.php b/inc/config.php index 91804429..5926eb1d 100644 --- a/inc/config.php +++ b/inc/config.php @@ -103,7 +103,7 @@ /* * ==================== - * Cache settings + * Cache, lock and queue settings * ==================== */ @@ -120,6 +120,7 @@ // $config['cache']['enabled'] = 'apc'; // $config['cache']['enabled'] = 'memcached'; // $config['cache']['enabled'] = 'redis'; + // $config['cache']['enabled'] = 'fs'; // Timeout for cached objects such as posts and HTML. $config['cache']['timeout'] = 60 * 60 * 48; // 48 hours @@ -142,6 +143,12 @@ // (this file will be explicitly loaded during cache hit, but not during cache miss). $config['cache_config'] = false; + // Define a lock driver. + $config['lock']['enabled'] = 'fs'; + + // Define a queue driver. + $config['queue']['enabled'] = 'fs'; // xD + /* * ==================== * Cookie settings diff --git a/inc/functions.php b/inc/functions.php index 8329e774..0cfdb72f 100755 --- a/inc/functions.php +++ b/inc/functions.php @@ -19,6 +19,8 @@ require_once 'inc/database.php'; require_once 'inc/events.php'; require_once 'inc/api.php'; require_once 'inc/mod/auth.php'; +require_once 'inc/lock.php'; +require_once 'inc/queue.php'; require_once 'inc/polyfill.php'; @include_once 'inc/lib/parsedown/Parsedown.php'; // fail silently, this isn't a critical piece of code @@ -93,6 +95,8 @@ function loadConfig() { 'db', 'api', 'cache', + 'lock', + 'queue', 'cookies', 'error', 'dir', @@ -1749,7 +1753,6 @@ function buildJavascript() { function checkDNSBL() { global $config; - if (isIPv6()) return; // No IPv6 support yet. @@ -2806,17 +2809,17 @@ function generation_strategy($fun, $array=array()) { global $config; $action = false; foreach ($config['generation_strategies'] as $s) { - if ($strategy = $s($fun, $array)) { + if ($action = $s($fun, $array)) { break; } } - switch ($strategy[0]) { + switch ($action[0]) { case 'immediate': return 'rebuild'; case 'defer': // Ok, it gets interesting here :) - Queue::add(serialize(array('build', $fun, $array))); + get_queue('generate')->push(serialize(array('build', $fun, $array, $action))); return 'ignore'; case 'build_on_load': return 'delete'; @@ -2832,5 +2835,32 @@ function strategy_smart_build($fun, $array) { } function strategy_sane($fun, $array) { global $config; - return false; + // Well, ideally a sane strategy would involve a more stringent checking, + // but let's at least have something to get the ball rolling :^) + + if (php_sapi_name() == 'cli') return false; + else if (isset($_POST['mod']) || isset($_POST['json_response'])) return false; + else if ($fun == 'sb_thread' || ($fun == 'sb_board' && $array[1] == 1)) return array('immediate'); + else return false; +} + +// My first, test strategy. +function strategy_first($fun, $array) { + switch ($fun) { + case 'sb_thread': + return array('defer'); + case 'sb_board': + if ($array[1] > 8) return array('build_on_load'); + else return array('defer'); + case 'sb_api': + return array('defer'); + case 'sb_catalog': + return array('defer'); + case 'sb_recent': + return array('build_on_load'); + case 'sb_sitemap': + return array('build_on_load'); + case 'sb_ukko': + return array('defer'); + } } diff --git a/smart_build.php b/smart_build.php index 7ca5fcbf..cfac446a 100644 --- a/smart_build.php +++ b/smart_build.php @@ -24,7 +24,7 @@ if (!$route) { } else { list ($fun, $args) = $route; - $reached = call_user_func_array($route); + $reached = call_user_func_array($fun, $args); } function die_404() { global $config; diff --git a/tmp/queue/generate/.gitkeep b/tmp/queue/generate/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tools/worker.php b/tools/worker.php new file mode 100755 index 00000000..e19fe1c6 --- /dev/null +++ b/tools/worker.php @@ -0,0 +1,31 @@ +#!/usr/bin/php +pop(2); + foreach ($q as $v) { + list($__, $func, $ary, $action) = unserialize($v); + echo "Starting to generate $func ".implode(" ", $ary)."... "; + + call_user_func_array($func, $ary); + + echo "done!\n"; + } + if (!$q) usleep(20000); // 0.02s +} -- cgit v1.3 From 8a46c7a0d54864d7f640e1972a23e78018368cc2 Mon Sep 17 00:00:00 2001 From: czaks Date: Thu, 9 Jun 2016 11:08:29 +0200 Subject: tesseract OCR support for spamfilters --- inc/config.php | 13 +++++++++++++ inc/functions.php | 2 +- post.php | 43 ++++++++++++++++++++++++++++++++++++++----- tmp/tesseract/.gitkeep | 0 4 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 tmp/tesseract/.gitkeep (limited to 'inc/config.php') diff --git a/inc/config.php b/inc/config.php index 5926eb1d..ebe17572 100644 --- a/inc/config.php +++ b/inc/config.php @@ -824,6 +824,15 @@ // Set this to true if you're using Linux and you can execute `md5sum` binary. $config['gnu_md5'] = false; + // Use Tesseract OCR to retrieve text from images, so you can use it as a spamfilter. + $config['tesseract_ocr'] = false; + + // Tesseract parameters + $config['tesseract_params'] = ''; + + // Tesseract preprocess command + $config['tesseract_preprocess_command'] = 'convert -monochrome %s -'; + // Number of posts in a "View Last X Posts" page $config['noko50_count'] = 50; // Number of posts a thread needs before it gets a "View Last X Posts" page. @@ -1015,6 +1024,10 @@ // Minify Javascript using http://code.google.com/p/minify/. $config['minify_js'] = false; + // Dispatch thumbnail loading and image configuration with JavaScript. It will need a certain javascript + // code to work. + $config['javascript_image_dispatch'] = false; + /* * ==================== * Video embedding diff --git a/inc/functions.php b/inc/functions.php index 154386b0..5a8f6000 100755 --- a/inc/functions.php +++ b/inc/functions.php @@ -2695,7 +2695,7 @@ function slugify($post) { elseif (isset ($post['body_nomarkup']) && $post['body_nomarkup']) $slug = $post['body_nomarkup']; elseif (isset ($post['body']) && $post['body']) - $slug = strip_html($post['body']); + $slug = strip_tags($post['body']); // Fix UTF-8 first $slug = mb_convert_encoding($slug, "UTF-8", "UTF-8"); diff --git a/post.php b/post.php index 4ea08ce3..bf723e01 100644 --- a/post.php +++ b/post.php @@ -652,14 +652,14 @@ if (isset($_POST['delete'])) { $post['filehash'] = md5($allhashes); } } - + if (!hasPermission($config['mod']['bypass_filters'], $board['uri'])) { - require_once 'inc/filters.php'; - + require_once 'inc/filters.php'; + do_filters($post); } - - if ($post['has_file']) { + + if ($post['has_file']) { foreach ($post['files'] as $key => &$file) { if ($file['is_an_image']) { if ($config['ie_mime_type_detection'] !== false) { @@ -787,6 +787,34 @@ if (isset($_POST['delete'])) { $file['thumbwidth'] = $size[0]; $file['thumbheight'] = $size[1]; } + + if ($config['tesseract_ocr']) { // Let's OCR it! + $fname = $file['tmp_name']; + + if ($file['height'] > 500 || $file['width'] > 500) { + $fname = $file['thumb']; + } + + if ($fname == 'spoiler') { // We don't have that much CPU time, do we? + } + else { + $tmpname = "tmp/tesseract/".rand(0,10000000); + + // Preprocess command is an ImageMagick b/w quantization + $error = shell_exec_error(sprintf($config['tesseract_preprocess_command'], escapeshellarg($fname)) . " | " . + 'tesseract stdin '.escapeshellarg($tmpname).' '.$config['tesseract_params']); + $tmpname .= ".txt"; + + $value = @file_get_contents($tmpname); + @unlink($tmpname); + + if ($value && trim($value)) { + // This one has an effect, that the body is appended to a post body. So you can write a correct + // spamfilter. + $post['body_nomarkup'] .= "".htmlspecialchars($value).""; + } + } + } if (!isset($dont_copy_file) || !$dont_copy_file) { if (isset($file['file_tmp'])) { @@ -827,6 +855,11 @@ if (isset($_POST['delete'])) { } } + // Do filters again if OCRing + if ($config['tesseract_ocr'] && !hasPermission($config['mod']['bypass_filters'], $board['uri'])) { + do_filters($post); + } + if (!hasPermission($config['mod']['postunoriginal'], $board['uri']) && $config['robot_enable'] && checkRobot($post['body_nomarkup'])) { undoImage($post); if ($config['robot_mute']) { diff --git a/tmp/tesseract/.gitkeep b/tmp/tesseract/.gitkeep new file mode 100644 index 00000000..e69de29b -- cgit v1.3 From 5e335a8564a76f0449d47f0767b4baa8d2cb9e91 Mon Sep 17 00:00:00 2001 From: czaks Date: Sun, 14 Aug 2016 16:24:17 +0200 Subject: preliminary inbound nntpchan support --- inc/config.php | 17 +++ install.php | 15 ++- install.sql | 19 +++ post.php | 359 +++++++++++++++++++++++++++++++++++++++++++-------------- 4 files changed, 322 insertions(+), 88 deletions(-) (limited to 'inc/config.php') diff --git a/inc/config.php b/inc/config.php index ebe17572..a8afdf7a 100644 --- a/inc/config.php +++ b/inc/config.php @@ -1689,6 +1689,23 @@ // Example: Adding the pre-markup post body to the API as "com_nomarkup". // $config['api']['extra_fields'] = array('body_nomarkup' => 'com_nomarkup'); +/* + * ================== + * NNTPChan settings + * ================== + */ + + $config['nntpchan'] = array(); + + // Enable NNTPChan integration + $config['nntpchan']['enabled'] = false; + + // NNTP server + $config['nntpchan']['server'] = "localhost:1119"; + + // Global dispatch array. Add your boards to it to enable them. + $config['nntpchan']['dispatch'] = array(); // 'overchan.test' => 'test' + /* * ==================== * Other/uncategorized diff --git a/install.php b/install.php index de4d3dd0..6f1617a8 100644 --- a/install.php +++ b/install.php @@ -1,7 +1,7 @@ vichan upgrade path. diff --git a/install.sql b/install.sql index bf9b92bf..3b390645 100644 --- a/install.sql +++ b/install.sql @@ -314,6 +314,25 @@ CREATE TABLE `pages` ( UNIQUE KEY `u_pages` (`name`,`board`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4; +-- -------------------------------------------------------- + +-- +-- Table structure for table `nntp_references` +-- + +CREATE TABLE `nntp_references` ( + `board` varchar(60) NOT NULL, + `id` int(11) unsigned NOT NULL, + `message_id` varchar(255) CHARACTER SET ascii NOT NULL, + `message_id_digest` varchar(40) CHARACTER SET ascii NOT NULL, + `own` tinyint(1) NOT NULL, + `headers` text, + PRIMARY KEY (`message_id_digest`), + UNIQUE KEY `message_id` (`message_id`), + UNIQUE KEY `u_board_id` (`board`, `id`) +) 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 */; diff --git a/post.php b/post.php index aa1b8235..4af09928 100644 --- a/post.php +++ b/post.php @@ -11,6 +11,160 @@ if ((!isset($_POST['mod']) || !$_POST['mod']) && $config['board_locked']) { error("Board is locked"); } +$dropped_post = false; + +if (isset($_GET['Newsgroups']) && $config['nntpchan']['enabled']) { + $_POST = array(); + $_POST['json_response'] = true; + + $headers = json_encode($_GET); + + if (!isset ($_GET['Message-Id'])) { + if (!isset ($_GET['Message-ID'])) { + error("NNTPChan: No message ID"); + } + else $msgid = $_GET['Message-ID']; + } + else $msgid = $_GET['Message-Id']; + + $groups = preg_split("/,\s*/", $_GET['Newsgroups']); + if (count($groups) != 1) { + error("NNTPChan: Messages can go to only one newsgroup"); + } + $group = $groups[0]; + + if (!isset($config['nntpchan']['dispatch'][$group])) { + error("NNTPChan: We don't synchronize $group"); + } + $xboard = $config['nntpchan']['dispatch'][$group]; + + $ref = null; + if (isset ($_GET['References'])) { + $refs = preg_split("/,\s*/", $_GET['References']); + + if (count($refs) > 1) { + error("NNTPChan: We don't support multiple references"); + } + + $ref = $refs[0]; + + $query = prepare("SELECT `board`,`id` FROM ``nntp_references`` WHERE `message_id` = :ref"); + $query->bindValue(':ref', $ref); + $query->execute() or error(db_error($query)); + + $ary = $query->fetchAll(PDO::FETCH_ASSOC); + + if (count($ary) == 0) { + error("NNTPChan: We don't have $ref that $msgid references"); + } + + $p_id = $ary[0]['id']; + $p_board = $ary[0]['board']; + + if ($p_board != $xboard) { + error("NNTPChan: Cross board references not allowed. Tried to reference $p_board on $xboard"); + } + + $_POST['thread'] = $p_id; + } + + $date = isset($_GET['Date']) ? strtotime($_GET['Date']) : time(); + + list($ct) = explode('; ', $_GET['Content-Type']); + + $query = prepare("SELECT COUNT(*) AS `c` FROM ``nntp_references`` WHERE `message_id` = :msgid"); + $query->bindValue(":msgid", $msgid); + $query->execute() or error(db_error($query)); + + $a = $query->fetch(PDO::FETCH_ASSOC); + if ($a['c'] > 0) { + error("NNTPChan: We already have this post. Post discarded."); + } + + if ($ct == 'text/plain') { + $content = file_get_contents("php://input"); + } + elseif ($ct == 'multipart/mixed' || $ct == 'multipart/form-data') { + _syslog(LOG_INFO, "MM: Files: ".print_r($GLOBALS, true)); + $content = ''; + + $tmpfiles = $_FILES['attachment']; + foreach ($tmpfiles as $id => $file) { + if ($file['type'] == 'text/plain') { + $content .= file_get_contents($file['tmp_name']); + unset($_FILES['attachment'][$id]); + } + elseif ($file['type'] == 'message/rfc822') { // Signed message, ignore for now + unset($_FILES['attachment'][$id]); + } + else { // A real attachment :^) + } + } + + $_FILES = $_FILES['attachment']; + + + } + else { + error("NNTPChan: Wrong mime type: $ct"); + } + + $_POST['subject'] = isset($_GET['Subject']) ? $_GET['Subject'] : ''; + $_POST['board'] = $xboard; + + if (isset ($_GET['From'])) { + list($name, $mail) = explode(" <", $_GET['From'], 2); + $mail = preg_replace('/>\s+$/', '', $mail); + + $_POST['name'] = $name; + //$_POST['email'] = $mail; + $_POST['email'] = ''; + } + + if (isset ($_GET['X_Sage'])) { + $_POST['email'] = 'sage'; + } + + $content = preg_replace_callback('/>>([0-9a-fA-F]{6,})/', function($id) use ($xboard) { + $id = $id[1]; + + $query = prepare("SELECT `board`,`id` FROM ``nntp_references`` WHERE `message_id_digest` LIKE :rule"); + $idx = $id . "%"; + $query->bindValue(':rule', $idx); + $query->execute() or error(db_error($query)); + + $ary = $query->fetchAll(PDO::FETCH_ASSOC); + if (count($ary) == 0) { + return ">>>>$id"; + } + else { + $ret = array(); + foreach ($ary as $v) { + if ($v['board'] != $xboard) { + $ret[] = ">>>/".$v['board']."/".$v['id']; + } + else { + $ret[] = ">>".$v['id']; + } + } + return implode($ret, ", "); + } + }, $content); + + $_POST['body'] = $content; + + $dropped_post = array( + 'date' => $date, + 'board' => $xboard, + 'msgid' => $msgid, + 'headers' => $headers, + 'from_nntp' => true, + ); +} +elseif (isset($_GET['Newsgroups'])) { + error("NNTPChan: NNTPChan support is disabled"); +} + if (isset($_POST['delete'])) { // Delete @@ -178,8 +332,8 @@ if (isset($_POST['delete'])) { header('Content-Type: text/json'); echo json_encode(array('success' => true)); } -} elseif (isset($_POST['post'])) { - if (!isset($_POST['body'], $_POST['board'])) +} elseif (isset($_POST['post']) || $dropped_post) { + if (!isset($_POST['body'], $_POST['board']) && !$dropped_post) error($config['error']['bot']); $post = array('board' => $_POST['board'], 'files' => array()); @@ -206,61 +360,67 @@ if (isset($_POST['delete'])) { } else $post['op'] = true; - // Check for CAPTCHA right after opening the board so the "return" link is in there - if ($config['recaptcha']) { - if (!isset($_POST['recaptcha_challenge_field']) || !isset($_POST['recaptcha_response_field'])) - error($config['error']['bot']); - // Check what reCAPTCHA has to say... - $resp = recaptcha_check_answer($config['recaptcha_private'], - $_SERVER['REMOTE_ADDR'], - $_POST['recaptcha_challenge_field'], - $_POST['recaptcha_response_field']); - if (!$resp->is_valid) { - error($config['error']['captcha']); + + if (!$dropped_post) { + // Check for CAPTCHA right after opening the board so the "return" link is in there + if ($config['recaptcha']) { + if (!isset($_POST['recaptcha_challenge_field']) || !isset($_POST['recaptcha_response_field'])) + error($config['error']['bot']); + // Check what reCAPTCHA has to say... + $resp = recaptcha_check_answer($config['recaptcha_private'], + $_SERVER['REMOTE_ADDR'], + $_POST['recaptcha_challenge_field'], + $_POST['recaptcha_response_field']); + if (!$resp->is_valid) { + error($config['error']['captcha']); + } } - } - if (!(($post['op'] && $_POST['post'] == $config['button_newtopic']) || - (!$post['op'] && $_POST['post'] == $config['button_reply']))) - error($config['error']['bot']); + if (!(($post['op'] && $_POST['post'] == $config['button_newtopic']) || + (!$post['op'] && $_POST['post'] == $config['button_reply']))) + error($config['error']['bot']); - // Check the referrer - if ($config['referer_match'] !== false && - (!isset($_SERVER['HTTP_REFERER']) || !preg_match($config['referer_match'], rawurldecode($_SERVER['HTTP_REFERER'])))) - error($config['error']['referer']); + // Check the referrer + if ($config['referer_match'] !== false && + (!isset($_SERVER['HTTP_REFERER']) || !preg_match($config['referer_match'], rawurldecode($_SERVER['HTTP_REFERER'])))) + error($config['error']['referer']); - checkDNSBL(); + checkDNSBL(); - // Check if banned - checkBan($board['uri']); + // Check if banned + checkBan($board['uri']); - if ($post['mod'] = isset($_POST['mod']) && $_POST['mod']) { - check_login(false); - if (!$mod) { - // Liar. You're not a mod. - error($config['error']['notamod']); - } + if ($post['mod'] = isset($_POST['mod']) && $_POST['mod']) { + check_login(false); + 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']); + $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['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']) { + $post['antispam_hash'] = checkSpam(array($board['uri'], isset($post['thread']) ? $post['thread'] : ($config['try_smarter'] && isset($_POST['page']) ? 0 - (int)$_POST['page'] : null))); + if ($post['antispam_hash'] === true) + error($config['error']['spam']); + } - if (!$post['mod']) { - $post['antispam_hash'] = checkSpam(array($board['uri'], isset($post['thread']) ? $post['thread'] : ($config['try_smarter'] && isset($_POST['page']) ? 0 - (int)$_POST['page'] : null))); - if ($post['antispam_hash'] === true) - error($config['error']['spam']); + if ($config['robot_enable'] && $config['robot_mute']) { + checkMute(); + } } - - if ($config['robot_enable'] && $config['robot_mute']) { - checkMute(); + else { + $mod = $post['mod'] = false; } //Check if thread exists @@ -371,28 +531,36 @@ if (isset($_POST['delete'])) { $post['email'] = str_replace(' ', '%20', htmlspecialchars($_POST['email'])); $post['body'] = $_POST['body']; $post['password'] = $_POST['password']; - $post['has_file'] = (!isset($post['embed']) && (($post['op'] && !isset($post['no_longer_require_an_image_for_op']) && $config['force_image_op']) || !empty($_FILES['file']['name']))); + $post['has_file'] = (!isset($post['embed']) && (($post['op'] && !isset($post['no_longer_require_an_image_for_op']) && $config['force_image_op']) || count($_FILES) > 0)); - if (!($post['has_file'] || isset($post['embed'])) || (($post['op'] && $config['force_body_op']) || (!$post['op'] && $config['force_body']))) { - $stripped_whitespace = preg_replace('/[\s]/u', '', $post['body']); - if ($stripped_whitespace == '') { - error($config['error']['tooshort_body']); + if (!$dropped_post) { + + if (!($post['has_file'] || isset($post['embed'])) || (($post['op'] && $config['force_body_op']) || (!$post['op'] && $config['force_body']))) { + $stripped_whitespace = preg_replace('/[\s]/u', '', $post['body']); + if ($stripped_whitespace == '') { + error($config['error']['tooshort_body']); + } } - } - if (!$post['op']) { - // Check if thread is locked - // but allow mods to post - if ($thread['locked'] && !hasPermission($config['mod']['postinlocked'], $board['uri'])) - error($config['error']['locked']); + if (!$post['op']) { + // Check if thread is locked + // but allow mods to post + if ($thread['locked'] && !hasPermission($config['mod']['postinlocked'], $board['uri'])) + error($config['error']['locked']); - $numposts = numPosts($post['thread']); + $numposts = numPosts($post['thread']); - if ($config['reply_hard_limit'] != 0 && $config['reply_hard_limit'] <= $numposts['replies']) - error($config['error']['reply_hard_limit']); + if ($config['reply_hard_limit'] != 0 && $config['reply_hard_limit'] <= $numposts['replies']) + error($config['error']['reply_hard_limit']); - if ($post['has_file'] && $config['image_hard_limit'] != 0 && $config['image_hard_limit'] <= $numposts['images']) - error($config['error']['image_hard_limit']); + if ($post['has_file'] && $config['image_hard_limit'] != 0 && $config['image_hard_limit'] <= $numposts['images']) + error($config['error']['image_hard_limit']); + } + } + else { + if (!$post['op']) { + $numposts = numPosts($post['thread']); + } } if ($post['has_file']) { @@ -442,7 +610,7 @@ if (isset($_POST['delete'])) { $trip = generate_tripcode($post['name']); $post['name'] = $trip[0]; - $post['trip'] = isset($trip[1]) ? $trip[1] : ''; + $post['trip'] = isset($trip[1]) ? $trip[1] : ''; // XX: Dropped posts and tripcodes $noko = false; if (strtolower($post['email']) == 'noko') { @@ -477,15 +645,17 @@ if (isset($_POST['delete'])) { if (empty($post['files'])) $post['has_file'] = false; - // Check for a file - if ($post['op'] && !isset($post['no_longer_require_an_image_for_op'])) { - if (!$post['has_file'] && $config['force_image_op']) - error($config['error']['noimage']); - } + if (!$dropped_post) { + // Check for a file + if ($post['op'] && !isset($post['no_longer_require_an_image_for_op'])) { + if (!$post['has_file'] && $config['force_image_op']) + error($config['error']['noimage']); + } - // Check for too many files - if (sizeof($post['files']) > $config['max_images']) - error($config['error']['toomanyimages']); + // Check for too many files + if (sizeof($post['files']) > $config['max_images']) + error($config['error']['toomanyimages']); + } if ($config['strip_combining_chars']) { $post['name'] = strip_combining_chars($post['name']); @@ -494,18 +664,19 @@ if (isset($_POST['delete'])) { $post['body'] = strip_combining_chars($post['body']); } - // Check string lengths - if (mb_strlen($post['name']) > 35) - error(sprintf($config['error']['toolong'], 'name')); - if (mb_strlen($post['email']) > 40) - error(sprintf($config['error']['toolong'], 'email')); - if (mb_strlen($post['subject']) > 100) - error(sprintf($config['error']['toolong'], 'subject')); - if (!$mod && mb_strlen($post['body']) > $config['max_body']) - error($config['error']['toolong_body']); - if (mb_strlen($post['password']) > 20) - error(sprintf($config['error']['toolong'], 'password')); - + if (!$dropped_post) { + // Check string lengths + if (mb_strlen($post['name']) > 35) + error(sprintf($config['error']['toolong'], 'name')); + if (mb_strlen($post['email']) > 40) + error(sprintf($config['error']['toolong'], 'email')); + if (mb_strlen($post['subject']) > 100) + error(sprintf($config['error']['toolong'], 'subject')); + if (!$mod && mb_strlen($post['body']) > $config['max_body']) + error($config['error']['toolong_body']); + if (mb_strlen($post['password']) > 20) + error(sprintf($config['error']['toolong'], 'password')); + } wordfilters($post['body']); $post['body'] = escape_markup_modifiers($post['body']); @@ -514,6 +685,7 @@ if (isset($_POST['delete'])) { $post['body'] .= "\n1"; } + if (!$dropped_post) if (($config['country_flags'] && !$config['allow_no_country']) || ($config['country_flags'] && $config['allow_no_country'] && !isset($_POST['no_country']))) { require 'inc/lib/geoip/geoip.inc'; $gi=geoip\geoip_open('inc/lib/geoip/GeoIPv6.dat', GEOIP_STANDARD); @@ -555,6 +727,7 @@ if (isset($_POST['delete'])) { $post['body'] .= "\n" . $_POST['tag'] . ""; } + if (!$dropped_post) if ($config['proxy_save'] && isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { $proxy = preg_replace("/[^0-9a-fA-F.,: ]/", '', $_SERVER['HTTP_X_FORWARDED_FOR']); $post['body'] .= "\n".$proxy.""; @@ -627,7 +800,7 @@ if (isset($_POST['delete'])) { } } - if (!hasPermission($config['mod']['bypass_filters'], $board['uri'])) { + if (!hasPermission($config['mod']['bypass_filters'], $board['uri']) && !$dropped_post) { require_once 'inc/filters.php'; do_filters($post); @@ -830,11 +1003,11 @@ if (isset($_POST['delete'])) { } // Do filters again if OCRing - if ($config['tesseract_ocr'] && !hasPermission($config['mod']['bypass_filters'], $board['uri'])) { + if ($config['tesseract_ocr'] && !hasPermission($config['mod']['bypass_filters'], $board['uri']) && !$dropped_post) { do_filters($post); } - if (!hasPermission($config['mod']['postunoriginal'], $board['uri']) && $config['robot_enable'] && checkRobot($post['body_nomarkup'])) { + if (!hasPermission($config['mod']['postunoriginal'], $board['uri']) && $config['robot_enable'] && checkRobot($post['body_nomarkup']) && !$dropped_post) { undoImage($post); if ($config['robot_mute']) { error(sprintf($config['error']['muted'], mute())); @@ -873,6 +1046,18 @@ if (isset($_POST['delete'])) { $post['id'] = $id = post($post); $post['slug'] = slugify($post); + + if ($dropped_post && $dropped_post['from_nntp']) { + $query = prepare("INSERT INTO ``nntp_references`` (`board`, `id`, `message_id`, `message_id_digest`, `own`, `headers`) VALUES ". + "(:board , :id , :message_id , :message_id_digest , false, :headers)"); + + $query->bindValue(':board', $dropped_post['board']); + $query->bindValue(':id', $id); + $query->bindValue(':message_id', $dropped_post['msgid']); + $query->bindValue(':message_id_digest', sha1($dropped_post['msgid'])); + $query->bindValue(':headers', $dropped_post['headers']); + $query->execute() or error(db_error($query)); + } insertFloodPost($post); // Handle cyclical threads -- cgit v1.3 From a779b96370d3353c8409d64b70c724fa783ad219 Mon Sep 17 00:00:00 2001 From: czaks Date: Mon, 15 Aug 2016 00:56:06 +0200 Subject: second iteration of nntpchan implementation --- inc/config.php | 24 +++++++- inc/nntpchan/nntpchan.php | 149 ++++++++++++++++++++++++++++++++++++++++++++++ inc/nntpchan/tests.php | 30 ++++++++++ post.php | 29 +++++++++ 4 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 inc/nntpchan/nntpchan.php create mode 100644 inc/nntpchan/tests.php (limited to 'inc/config.php') diff --git a/inc/config.php b/inc/config.php index a8afdf7a..bfb774a8 100644 --- a/inc/config.php +++ b/inc/config.php @@ -1693,6 +1693,11 @@ * ================== * NNTPChan settings * ================== + */ + +/* + * Please keep in mind that NNTPChan support in vichan isn't finished yet / is in an experimental + * state. Please join #nntpchan on Rizon in order to peer with someone. */ $config['nntpchan'] = array(); @@ -1703,9 +1708,26 @@ // NNTP server $config['nntpchan']['server'] = "localhost:1119"; - // Global dispatch array. Add your boards to it to enable them. + // Global dispatch array. Add your boards to it to enable them. Please make + // sure that this setting is set in a global context. $config['nntpchan']['dispatch'] = array(); // 'overchan.test' => 'test' + // Trusted peer - an IP address of your NNTPChan instance. This peer will have + // increased capabilities, eg.: will evade spamfilter. + $config['nntpchan']['trusted_peer'] = '127.0.0.1'; + + // Salt for message ID generation. Keep it long and secure. + $config['nntpchan']['salt'] = 'change_me+please'; + + // A local message ID domain. Make sure to change it. + $config['nntpchan']['domain'] = 'example.vichan.net'; + + // An NNTPChan group name. + // Please set this setting in your board/config.php, not globally. + $config['nntpchan']['group'] = false; // eg. 'overchan.test' + + + /* * ==================== * Other/uncategorized diff --git a/inc/nntpchan/nntpchan.php b/inc/nntpchan/nntpchan.php new file mode 100644 index 00000000..25215e94 --- /dev/null +++ b/inc/nntpchan/nntpchan.php @@ -0,0 +1,149 @@ +"; +} + + +function gen_nntp($headers, $files) { + if (count($files) == 0) { + } + else if (count($files) == 1 && $files[0]['type'] == 'text/plain') { + $content = $files[0]['text'] . "\r\n"; + $headers['Content-Type'] = "text/plain; charset=UTF-8"; + } + else { + $boundary = sha1($headers['Message-Id']); + $content = ""; + $headers['Content-Type'] = "multipart/mixed; boundary=$boundary"; + foreach ($files as $file) { + $content .= "--$boundary\r\n"; + if (isset($file['name'])) { + $file['name'] = preg_replace('/[\r\n\0"]/', '', $file['name']); + $content .= "Content-Disposition: form-data; filename=\"$file[name]\"; name=\"attachment\"\r\n"; + } + $type = explode('/', $file['type'])[0]; + if ($type == 'text') { + $file['type'] .= '; charset=UTF-8'; + } + $content .= "Content-Type: $file[type]\r\n"; + if ($type != 'text' && $type != 'message') { + $file['text'] = base64_encode($file['text']); + $content .= "Content-Transfer-Encoding: base64\r\n"; + } + $content .= "\r\n"; + $content .= $file['text']; + $content .= "\r\n"; + } + $content .= "--$boundary--\r\n"; + + $headers['Mime-Version'] = '1.0'; + } + //$headers['Content-Length'] = strlen($content); + $headers['Date'] = date('r', $headers['Date']); + $out = ""; + foreach ($headers as $id => $val) { + $val = str_replace("\n", "\n\t", $val); + $out .= "$id: $val\r\n"; + } + $out .= "\r\n"; + $out .= $content; + return $out; +} + +function nntp_publish($msg, $id) { + $s = fsockopen("tcp://localhost:1119"); + fgets($s); + fputs($s, "MODE STREAM\r\n"); + fgets($s); + fputs($s, "TAKETHIS $id\r\n"); + fputs($s, $msg); + fputs($s, "\r\n.\r\n"); + fgets($s); + fclose($s); +} + +function post2nntp($post, $msgid) { + global $config; + + $headers = array(); + $files = array(); + + $headers['Message-Id'] = $msgid; + $headers['Newsgroups'] = $config['nntpchan']['group']; + $headers['Date'] = time(); + $headers['Subject'] = $post['subject'] ? $post['subject'] : "None"; + $headers['From'] = $post['name'] . " "; + + if ($post['email'] == 'sage') { + $headers['X-Sage'] = true; + } + + if (!$post['op']) { + // Get muh parent + $query = prepare("SELECT `message_id` FROM ``nntp_references`` WHERE `board` = :board AND `id` = :id"); + $query->bindValue(':board', $post['board']); + $query->bindValue(':id', $post['thread']); + $query->execute() or error(db_error($query)); + + if ($result = $query->fetch(PDO::FETCH_ASSOC)) { + $headers['References'] = $result['message_id']; + } + else { + return false; // We don't have OP. Discarding. + } + } + + // Let's parse the body a bit. + $body = trim($post['body_nomarkup']); + $body = preg_replace('/\r?\n/', "\r\n", $body); + $body = preg_replace_callback('@>>(>/([a-zA-Z0-9_+-]+)/)?([0-9]+)@', function($o) use ($post) { + if ($o[1]) { + $board = $o[2]; + } + else { + $board = $post['board']; + } + $id = $o[3]; + + $query = prepare("SELECT `message_id_digest` FROM ``nntp_references`` WHERE `board` = :board AND `id` = :id"); + $query->bindValue(':board', $board); + $query->bindValue(':id', $id); + $query->execute() or error(db_error($query)); + + if ($result = $query->fetch(PDO::FETCH_ASSOC)) { + return ">>".substr($result['message_id_digest'], 0, 16); + } + else { + return $o[0]; // Should send URL imo + } + }, $body); + $body = preg_replace('/>>>>([0-9a-fA-F])+/', '>>\1', $body); + + + $files[] = array('type' => 'text/plain', 'text' => $body); + + foreach ($post['files'] as $id => $file) { + $fc = array(); + + $fc['type'] = $file['type']; + $fc['text'] = file_get_contents($file['file_path']); + $fc['name'] = $file['name']; + + $files[] = $fc; + } + + return array($headers, $files); +} diff --git a/inc/nntpchan/tests.php b/inc/nntpchan/tests.php new file mode 100644 index 00000000..a63789d7 --- /dev/null +++ b/inc/nntpchan/tests.php @@ -0,0 +1,30 @@ + "czaks ", "Message-Id" => "<1234.0000.".$time."@example.vichan.net>", "Newsgroups" => "overchan.test", "Date" => time(), "Subject" => "None"], +[['type' => 'text/plain', 'text' => "THIS IS A NEW TEST THREAD"]]); +echo "\n@@@@ Single msg:\n"; +echo $m1 = gennntp(["From" => "czaks ", "Message-Id" => "<1234.1234.".$time."@example.vichan.net>", "Newsgroups" => "overchan.test", "Date" => time(), "Subject" => "None", "References" => "<1234.0000.".$time."@example.vichan.net>"], +[['type' => 'text/plain', 'text' => "hello world, with no image :("]]); +echo "\n@@@@ Single msg and pseudoimage:\n"; +echo $m2 = gennntp(["From" => "czaks ", "Message-Id" => "<1234.2137.".$time."@example.vichan.net>", "Newsgroups" => "overchan.test", "Date" => time(), "Subject" => "None", "References" => "<1234.0000.".$time."@example.vichan.net>"], +[['type' => 'text/plain', 'text' => "hello world, now with an image!"], + ['type' => 'image/gif', 'text' => base64_decode("R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="), 'name' => "urgif.gif"]]); +echo "\n@@@@ Single msg and two pseudoimages:\n"; +echo $m3 = gennntp(["From" => "czaks ", "Message-Id" => "<1234.1488.".$time."@example.vichan.net>", "Newsgroups" => "overchan.test", "Date" => time(), "Subject" => "None", "References" => "<1234.0000.".$time."@example.vichan.net>"], +[['type' => 'text/plain', 'text' => "hello world, now WITH TWO IMAGES!!!"], + ['type' => 'image/gif', 'text' => base64_decode("R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="), 'name' => "urgif.gif"], + ['type' => 'image/gif', 'text' => base64_decode("R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="), 'name' => "urgif2.gif"]]); +shoveitup($m0, "<1234.0000.".$time."@example.vichan.net>"); +sleep(1); +shoveitup($m1, "<1234.1234.".$time."@example.vichan.net>"); +sleep(1); +shoveitup($m2, "<1234.2137.".$time."@example.vichan.net>"); +shoveitup($m3, "<1234.1488.".$time."@example.vichan.net>"); + diff --git a/post.php b/post.php index 4590a534..1e4edcee 100644 --- a/post.php +++ b/post.php @@ -13,7 +13,12 @@ if ((!isset($_POST['mod']) || !$_POST['mod']) && $config['board_locked']) { $dropped_post = false; +// Is it a post coming from NNTP? Let's extract it and pretend it's a normal post. if (isset($_GET['Newsgroups']) && $config['nntpchan']['enabled']) { + if ($_SERVER['REMOTE_ADDR'] != $config['nntpchan']['trusted_peer']) { + error("NNTPChan: Forbidden. $_SERVER[REMOTE_ADDR] is not a trusted peer"); + } + $_POST = array(); $_POST['json_response'] = true; @@ -1057,7 +1062,31 @@ if (isset($_POST['delete'])) { $query->bindValue(':message_id_digest', sha1($dropped_post['msgid'])); $query->bindValue(':headers', $dropped_post['headers']); $query->execute() or error(db_error($query)); + } // ^^^^^ For inbound posts ^^^^^ + elseif ($config['nntpchan']['enabled'] && $config['nntpchan']['group']) { + // vvvvv For outbound posts vvvvv + + require_once('inc/nntpchan/nntpchan.php'); + $msgid = gen_msgid($post['board'], $post['id']); + + list($headers, $files) = post2nntp($post, $msgid); + + $message = gen_nntp($headers, $files); + + $query = prepare("INSERT INTO ``nntp_references`` (`board`, `id`, `message_id`, `message_id_digest`, `own`, `headers`) VALUES ". + "(:board , :id , :message_id , :message_id_digest , true , :headers)"); + + $query->bindValue(':board', $post['board']); + $query->bindValue(':id', $post['id']); + $query->bindValue(':message_id', $msgid); + $query->bindValue(':message_id_digest', sha1($msgid)); + $query->bindValue(':headers', json_encode($headers)); + $query->execute() or error(db_error($query)); + + // Let's broadcast it! + nntp_publish($message, $msgid); } + insertFloodPost($post); // Handle cyclical threads -- cgit v1.3