From 21155cbb0666ed6c4fef2e1de0ea5af512768312 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Sat, 14 Dec 2024 00:15:37 +0100 Subject: [PATCH 001/258] functions.php: optimize listBoards a bit --- inc/functions.php | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/inc/functions.php b/inc/functions.php index a355e53c..767dfc06 100644 --- a/inc/functions.php +++ b/inc/functions.php @@ -745,24 +745,23 @@ function hasPermission($action = null, $board = null, $_mod = null) { function listBoards($just_uri = false) { global $config; - $just_uri ? $cache_name = 'all_boards_uri' : $cache_name = 'all_boards'; + $cache_name = $just_uri ? 'all_boards_uri' : 'all_boards'; - if ($config['cache']['enabled'] && ($boards = cache::get($cache_name))) + if ($config['cache']['enabled'] && ($boards = cache::get($cache_name))) { return $boards; - - if (!$just_uri) { - $query = query("SELECT * FROM ``boards`` ORDER BY `uri`") or error(db_error()); - $boards = $query->fetchAll(); - } else { - $boards = array(); - $query = query("SELECT `uri` FROM ``boards``") or error(db_error()); - while ($board = $query->fetchColumn()) { - $boards[] = $board; - } } - if ($config['cache']['enabled']) + if (!$just_uri) { + $query = query('SELECT * FROM ``boards`` ORDER BY `uri`'); + $boards = $query->fetchAll(); + } else { + $query = query('SELECT `uri` FROM ``boards``'); + $boards = $query->fetchAll(\PDO::FETCH_COLUMN); + } + + if ($config['cache']['enabled']) { cache::set($cache_name, $boards); + } return $boards; } From b03130fcb45a9286c26ff8a19e5a9523e895e5ea Mon Sep 17 00:00:00 2001 From: Zankaria Date: Tue, 17 Dec 2024 17:13:10 +0100 Subject: [PATCH 002/258] anti-bot.php: retry transaction upon deadlock --- inc/anti-bot.php | 69 ++++++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/inc/anti-bot.php b/inc/anti-bot.php index 1f1c885b..7a76b5cd 100644 --- a/inc/anti-bot.php +++ b/inc/anti-bot.php @@ -196,49 +196,56 @@ function _create_antibot($pdo, $board, $thread) { $antibot = new AntiBot(array($board, $thread)); try { - $pdo->beginTransaction(); + retry_on_deadlock(3, function() use ($config, $pdo, $thread, $board, $antibot) { + try { + $pdo->beginTransaction(); - // Delete old expired antispam, skipping those with NULL expiration timestamps (infinite lifetime). - if (!isset($purged_old_antispam) && $config['auto_maintenance']) { - $purged_old_antispam = true; - purge_old_antispam(); - } + // Delete old expired antispam, skipping those with NULL expiration timestamps (infinite lifetime). + if (!isset($purged_old_antispam) && $config['auto_maintenance']) { + $purged_old_antispam = true; + purge_old_antispam(); + } - // Keep the now invalid timestamps around for a bit to enable users to post if they're still on an old version of - // the HTML page. - // By virtue of existing, we know that we're making a new version of the page, and the user from now on may just reload. - if ($thread) { - $query = prepare('UPDATE ``antispam`` SET `expires` = UNIX_TIMESTAMP() + :expires WHERE `board` = :board AND `thread` = :thread AND `expires` IS NULL'); - } else { - $query = prepare('UPDATE ``antispam`` SET `expires` = UNIX_TIMESTAMP() + :expires WHERE `board` = :board AND `thread` IS NULL AND `expires` IS NULL'); - } + // Keep the now invalid timestamps around for a bit to enable users to post if they're still on an old version of + // the HTML page. + // By virtue of existing, we know that we're making a new version of the page, and the user from now on may just reload. + if ($thread) { + $query = prepare('UPDATE ``antispam`` SET `expires` = UNIX_TIMESTAMP() + :expires WHERE `board` = :board AND `thread` = :thread AND `expires` IS NULL'); + } else { + $query = prepare('UPDATE ``antispam`` SET `expires` = UNIX_TIMESTAMP() + :expires WHERE `board` = :board AND `thread` IS NULL AND `expires` IS NULL'); + } - $query->bindValue(':board', $board); - if ($thread) { - $query->bindValue(':thread', $thread); - } - $query->bindValue(':expires', $config['spam']['hidden_inputs_expire']); - // Throws on error. - $query->execute(); + $query->bindValue(':board', $board); + if ($thread) { + $query->bindValue(':thread', $thread); + } + $query->bindValue(':expires', $config['spam']['hidden_inputs_expire']); + // Throws on error. + $query->execute(); - $hash = $antibot->hash(); + $hash = $antibot->hash(); - // Insert an antispam with infinite life as the HTML page of a thread might last well beyond the expiry date. - $query = prepare('INSERT INTO ``antispam`` VALUES (:board, :thread, :hash, UNIX_TIMESTAMP(), NULL, 0)'); - $query->bindValue(':board', $board); - $query->bindValue(':thread', $thread); - $query->bindValue(':hash', $hash); - // Throws on error. - $query->execute(); + // Insert an antispam with infinite life as the HTML page of a thread might last well beyond the expiry date. + $query = prepare('INSERT INTO ``antispam`` VALUES (:board, :thread, :hash, UNIX_TIMESTAMP(), NULL, 0)'); + $query->bindValue(':board', $board); + $query->bindValue(':thread', $thread); + $query->bindValue(':hash', $hash); + // Throws on error. + $query->execute(); - $pdo->commit(); + $pdo->commit(); + } catch (\Exception $e) { + $pdo->rollBack(); + throw $e; + } + }); } catch (\PDOException $e) { $pdo->rollBack(); if ($e->errorInfo === null || $e->errorInfo[1] != MYSQL_ER_LOCK_DEADLOCK) { throw $e; } else { - error_log('Deadlock on _create_antibot while inserting, skipping'); + \error_log('5 or more deadlocks on _create_antibot while inserting, skipping'); } } From b197c9ed4308a6051e7537069ab44c24067978ee Mon Sep 17 00:00:00 2001 From: Zankaria Date: Tue, 17 Dec 2024 17:16:59 +0100 Subject: [PATCH 003/258] docker: remove unused options in mysql --- compose.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/compose.yml b/compose.yml index c04c85f9..526e18c6 100644 --- a/compose.yml +++ b/compose.yml @@ -28,8 +28,6 @@ services: #MySQL Service db: image: mysql:8.0.35 - restart: unless-stopped - tty: true ports: - "3306:3306" environment: From 1d41ffbe4f1acf2c12febe290a33507d7042fab0 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Tue, 17 Dec 2024 17:18:03 +0100 Subject: [PATCH 004/258] anti-bot.php: include missing variable --- inc/anti-bot.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/anti-bot.php b/inc/anti-bot.php index 7a76b5cd..cf82dcc8 100644 --- a/inc/anti-bot.php +++ b/inc/anti-bot.php @@ -196,7 +196,7 @@ function _create_antibot($pdo, $board, $thread) { $antibot = new AntiBot(array($board, $thread)); try { - retry_on_deadlock(3, function() use ($config, $pdo, $thread, $board, $antibot) { + retry_on_deadlock(3, function() use ($config, $pdo, $thread, $board, $antibot, $purged_old_antispam) { try { $pdo->beginTransaction(); From 71416afc75cab86692560f3d454f924b23a7adbe Mon Sep 17 00:00:00 2001 From: Zankaria Date: Tue, 15 Oct 2024 18:35:32 +0200 Subject: [PATCH 005/258] UserPostQueries.php: add user post queries class --- inc/Data/PageFetchResult.php | 15 +++++ inc/Data/UserPostQueries.php | 115 +++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 inc/Data/PageFetchResult.php create mode 100644 inc/Data/UserPostQueries.php diff --git a/inc/Data/PageFetchResult.php b/inc/Data/PageFetchResult.php new file mode 100644 index 00000000..b33e7ac2 --- /dev/null +++ b/inc/Data/PageFetchResult.php @@ -0,0 +1,15 @@ +pdo = $pdo; + } + + /** + * Fetch a page of user posts. + * + * @param array $board_uris The uris of the boards that should be included. + * @param string $ip The IP of the target user. + * @param integer $page_size The Number of posts that should be fetched. + * @param string|null $cursor The directional cursor to fetch the next or previous page. Null to start from the beginning. + * @return PageFetchResult + */ + public function fetchPaginatedByIp(array $board_uris, string $ip, int $page_size, ?string $cursor = null): PageFetchResult { + // Decode the cursor. + if ($cursor !== null) { + list($cursor_type, $uri_id_cursor_map) = Net\decode_cursor($cursor); + } else { + // Defaults if $cursor is an invalid string. + $cursor_type = null; + $uri_id_cursor_map = []; + } + $next_cursor_map = []; + $prev_cursor_map = []; + $rows = []; + + foreach ($board_uris as $uri) { + // Extract the cursor relative to the board. + $id_cursor = false; + if (isset($uri_id_cursor_map[$uri])) { + $value = $uri_id_cursor_map[$uri]; + if (\is_numeric($value)) { + $id_cursor = (int)$value; + } + } + + if ($id_cursor === false) { + $query = $this->pdo->prepare(sprintf('SELECT * FROM `posts_%s` WHERE `ip` = :ip ORDER BY `sticky` DESC, `id` DESC LIMIT :limit', $uri)); + $query->bindValue(':ip', $ip); + $query->bindValue(':limit', $page_size + 1, \PDO::PARAM_INT); // Always fetch more. + $query->execute(); + $posts = $query->fetchAll(\PDO::FETCH_ASSOC); + } elseif ($cursor_type === self::CURSOR_TYPE_NEXT) { + $query = $this->pdo->prepare(sprintf('SELECT * FROM `posts_%s` WHERE `ip` = :ip AND `id` <= :start_id ORDER BY `sticky` DESC, `id` DESC LIMIT :limit', $uri)); + $query->bindValue(':ip', $ip); + $query->bindValue(':start_id', $id_cursor, \PDO::PARAM_INT); + $query->bindValue(':limit', $page_size + 2, \PDO::PARAM_INT); // Always fetch more. + $query->execute(); + $posts = $query->fetchAll(\PDO::FETCH_ASSOC); + } elseif ($cursor_type === self::CURSOR_TYPE_PREV) { + $query = $this->pdo->prepare(sprintf('SELECT * FROM `posts_%s` WHERE `ip` = :ip AND `id` >= :start_id ORDER BY `sticky` ASC, `id` ASC LIMIT :limit', $uri)); + $query->bindValue(':ip', $ip); + $query->bindValue(':start_id', $id_cursor, \PDO::PARAM_INT); + $query->bindValue(':limit', $page_size + 2, \PDO::PARAM_INT); // Always fetch more. + $query->execute(); + $posts = \array_reverse($query->fetchAll(\PDO::FETCH_ASSOC)); + } else { + throw new \RuntimeException("Unknown cursor type '$cursor_type'"); + } + + $posts_count = \count($posts); + + if ($posts_count === $page_size + 2) { + $has_extra_prev_post = true; + $has_extra_end_post = true; + } elseif ($posts_count === $page_size + 1) { + $has_extra_prev_post = $id_cursor !== false && $id_cursor === (int)$posts[0]['id']; + $has_extra_end_post = !$has_extra_prev_post; + } else { + $has_extra_prev_post = false; + $has_extra_end_post = false; + } + + // Since we fetched one post bellow and/or above the limit, we always know if there are any posts after the current page. + + // Get the previous cursor, if any. + if ($has_extra_prev_post) { + \array_shift($posts); + $posts_count--; + // Select the most recent post. + $prev_cursor_map[$uri] = $posts[0]['id']; + } + // Get the next cursor, if any. + if ($has_extra_end_post) { + \array_pop($posts); + // Select the oldest post. + $next_cursor_map[$uri] = $posts[$posts_count - 2]['id']; + } + + $rows[$uri] = $posts; + } + + $res = new PageFetchResult(); + $res->by_uri = $rows; + $res->cursor_prev = !empty($prev_cursor_map) ? Net\encode_cursor(self::CURSOR_TYPE_PREV, $prev_cursor_map) : null; + $res->cursor_next = !empty($next_cursor_map) ? Net\encode_cursor(self::CURSOR_TYPE_NEXT, $next_cursor_map) : null; + + return $res; + } +} \ No newline at end of file From 3c2bc572450b154c3943084ea0ab9e47a68b263e Mon Sep 17 00:00:00 2001 From: Zankaria Date: Tue, 15 Oct 2024 19:00:05 +0200 Subject: [PATCH 006/258] pages.php: use UserPostQueries for mod_page_ip --- inc/mod/pages.php | 121 ++++++++++++---------------------------------- 1 file changed, 30 insertions(+), 91 deletions(-) diff --git a/inc/mod/pages.php b/inc/mod/pages.php index bcb324b0..37b282c4 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -3,13 +3,9 @@ * Copyright (c) 2010-2013 Tinyboard Development Group */ use Vichan\Context; -use Vichan\Data\ReportQueries; -use Vichan\Functions\Format; +use Vichan\Data\{UserPostQueries, ReportQueries}; use Vichan\Functions\Net; -use function Vichan\Functions\Net\decode_cursor; -use function Vichan\Functions\Net\encode_cursor; - defined('TINYBOARD') or exit; function _link_or_copy(string $target, string $link): bool { @@ -870,7 +866,7 @@ function mod_ip_remove_note(Context $ctx, $ip, $id) { header('Location: ?/IP/' . $ip . '#notes', true, $config['redirect_http']); } -function mod_ip(Context $ctx, $ip, string $encoded_cursor = '') { +function mod_ip(Context $ctx, $ip, string $encoded_cursor = null) { global $config, $mod; if (filter_var($ip, FILTER_VALIDATE_IP) === false) @@ -924,98 +920,41 @@ function mod_ip(Context $ctx, $ip, string $encoded_cursor = '') { $args['hostname'] = rDNS($ip); } - // Decode the cursor. - list($cursor_type, $board_id_cursor_map) = decode_cursor($encoded_cursor); - $post_per_page = $config['mod']['ip_recentposts']; - $next_cursor_map = []; - $prev_cursor_map = []; - $boards = listBoards(); + + $queryable_uris = []; foreach ($boards as $board) { $uri = $board['uri']; - openBoard($uri); if (hasPermission($config['mod']['show_ip'], $uri)) { - // Extract the cursor relative to the board. - $id_cursor = false; - if (isset($board_id_cursor_map[$uri])) { - $value = $board_id_cursor_map[$uri]; - if (is_numeric($value)) { - $id_cursor = (int)$value; - } - } - - if ($id_cursor === false) { - $query = prepare(sprintf('SELECT * FROM `posts_%s` WHERE `ip` = :ip ORDER BY `sticky` DESC, `id` DESC LIMIT :limit', $uri)); - $query->bindValue(':ip', $ip); - $query->bindValue(':limit', $post_per_page + 1, PDO::PARAM_INT); // Always fetch more. - $query->execute(); - $posts = $query->fetchAll(PDO::FETCH_ASSOC); - } elseif ($cursor_type === 'n') { - $query = prepare(sprintf('SELECT * FROM `posts_%s` WHERE `ip` = :ip AND `id` <= :start_id ORDER BY `sticky` DESC, `id` DESC LIMIT :limit', $uri)); - $query->bindValue(':ip', $ip); - $query->bindValue(':start_id', $id_cursor, PDO::PARAM_INT); - $query->bindValue(':limit', $post_per_page + 2, PDO::PARAM_INT); // Always fetch more. - $query->execute(); - $posts = $query->fetchAll(PDO::FETCH_ASSOC); - } elseif ($cursor_type === 'p') { - // FIXME - $query = prepare(sprintf('SELECT * FROM `posts_%s` WHERE `ip` = :ip AND `id` >= :start_id ORDER BY `sticky` ASC, `id` ASC LIMIT :limit', $uri)); - $query->bindValue(':ip', $ip); - $query->bindValue(':start_id', $id_cursor, PDO::PARAM_INT); - $query->bindValue(':limit', $post_per_page + 2, PDO::PARAM_INT); // Always fetch more. - $query->execute(); - $posts = array_reverse($query->fetchAll(PDO::FETCH_ASSOC)); - } else { - throw new RuntimeException("Unknown cursor type '$cursor_type'"); - } - - $posts_count = count($posts); - - if ($posts_count === $post_per_page + 2) { - $has_extra_prev_post = true; - $has_extra_end_post = true; - } elseif ($posts_count === $post_per_page + 1) { - $has_extra_prev_post = $id_cursor !== false && $posts[0]['id'] == $id_cursor; - $has_extra_end_post = !$has_extra_prev_post; - } else { - $has_extra_prev_post = false; - $has_extra_end_post = false; - } - - // Get the previous cursor, if any. - if ($has_extra_prev_post) { - // Select the most recent post. - $prev_cursor_map[$uri] = $posts[1]['id']; - array_shift($posts); - $posts_count--; - } - // Get the next cursor, if any. - if ($has_extra_end_post) { - // Since we fetched 1 above the limit, we always know if there are any posts after the current page. - // Query orders by DESC, so the SECOND last post has the lowest ID. - array_pop($posts); - $next_cursor_map[$uri] = $posts[$posts_count - 2]['id']; - } - - // Finally load the post contents and build them. - foreach ($posts as $post) { - if (!$post['thread']) { - $po = new Thread($post, '?/', $mod, false); - } else { - $po = new Post($post, '?/', $mod); - } - - if (!isset($args['posts'][$uri])) { - $args['posts'][$uri] = [ 'board' => $board, 'posts' => [] ]; - } - $args['posts'][$uri]['posts'][] = $po->build(true); - } + $queryable_uris[] = $uri; } } - // Build the cursors. - $args['cursor_prev'] = !empty($encoded_cursor) ? encode_cursor('p', $prev_cursor_map) : false; - $args['cursor_next'] = !empty($next_cursor_map) ? encode_cursor('n', $next_cursor_map) : false; + $queries = $ctx->get(UserPostQueries::class); + $result = $queries->fetchPaginatedByIp($queryable_uris, $ip, $config['mod']['ip_recentposts'], $encoded_cursor); + + $args['cursor_prev'] = $result->cursor_prev; + $args['cursor_next'] = $result->cursor_next; + + foreach($boards as $board) { + $uri = $board['uri']; + // The Thread and Post classes rely on some implicit board parameter set by openBoard. + openBoard($uri); + + // Finally load the post contents and build them. + foreach ($result->by_uri[$uri] as $post) { + if (!$post['thread']) { + $po = new Thread($post, '?/', $mod, false); + } else { + $po = new Post($post, '?/', $mod); + } + + if (!isset($args['posts'][$uri])) { + $args['posts'][$uri] = [ 'board' => $board, 'posts' => [] ]; + } + $args['posts'][$uri]['posts'][] = $po->build(true); + } + } $args['boards'] = $boards; $args['token'] = make_secure_link_token('ban'); From 0b4ac333c7c9d7b9aaae187ff4c486022137bd2a Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 11 Dec 2024 17:31:57 +0100 Subject: [PATCH 007/258] context.php: use UserPostQueries --- inc/context.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/inc/context.php b/inc/context.php index b9e768e1..c744f9d8 100644 --- a/inc/context.php +++ b/inc/context.php @@ -2,7 +2,7 @@ namespace Vichan; use Vichan\Data\Driver\CacheDriver; -use Vichan\Data\ReportQueries; +use Vichan\Data\{ReportQueries, UserPostQueries}; defined('TINYBOARD') or exit; @@ -45,6 +45,9 @@ function build_context(array $config): Context { $auto_maintenance = (bool)$c->get('config')['auto_maintenance']; $pdo = $c->get(\PDO::class); return new ReportQueries($pdo, $auto_maintenance); + }, + UserPostQueries::class => function($c) { + return new UserPostQueries($c->get(\PDO::class)); } ]); } From 49b0ade2d3c3223413e6f6867a81ca8a716185bd Mon Sep 17 00:00:00 2001 From: Zankaria Date: Tue, 17 Dec 2024 17:55:57 +0100 Subject: [PATCH 008/258] lib: remove vendored library --- inc/lib/IP/LICENSE | 20 - inc/lib/IP/Lifo/IP/BC.php | 293 --------------- inc/lib/IP/Lifo/IP/CIDR.php | 706 ------------------------------------ inc/lib/IP/Lifo/IP/IP.php | 207 ----------- 4 files changed, 1226 deletions(-) delete mode 100755 inc/lib/IP/LICENSE delete mode 100755 inc/lib/IP/Lifo/IP/BC.php delete mode 100755 inc/lib/IP/Lifo/IP/CIDR.php delete mode 100755 inc/lib/IP/Lifo/IP/IP.php diff --git a/inc/lib/IP/LICENSE b/inc/lib/IP/LICENSE deleted file mode 100755 index fb315548..00000000 --- a/inc/lib/IP/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2013 Jason Morriss - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished -to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - diff --git a/inc/lib/IP/Lifo/IP/BC.php b/inc/lib/IP/Lifo/IP/BC.php deleted file mode 100755 index 26a2c2b7..00000000 --- a/inc/lib/IP/Lifo/IP/BC.php +++ /dev/null @@ -1,293 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace Lifo\IP; - -/** - * BCMath helper class. - * - * Provides a handful of BCMath routines that are not included in the native - * PHP library. - * - * Note: The Bitwise functions operate on fixed byte boundaries. For example, - * comparing the following numbers uses X number of bits: - * 0xFFFF and 0xFF will result in comparison of 16 bits. - * 0xFFFFFFFF and 0xF will result in comparison of 32 bits. - * etc... - * - */ -abstract class BC -{ - // Some common (maybe useless) constants - const MAX_INT_32 = '2147483647'; // 7FFFFFFF - const MAX_UINT_32 = '4294967295'; // FFFFFFFF - const MAX_INT_64 = '9223372036854775807'; // 7FFFFFFFFFFFFFFF - const MAX_UINT_64 = '18446744073709551615'; // FFFFFFFFFFFFFFFF - const MAX_INT_96 = '39614081257132168796771975167'; // 7FFFFFFFFFFFFFFFFFFFFFFF - const MAX_UINT_96 = '79228162514264337593543950335'; // FFFFFFFFFFFFFFFFFFFFFFFF - const MAX_INT_128 = '170141183460469231731687303715884105727'; // 7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF - const MAX_UINT_128 = '340282366920938463463374607431768211455'; // FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF - - /** - * BC Math function to convert a HEX string into a DECIMAL - */ - public static function bchexdec($hex) - { - if (strlen($hex) == 1) { - return hexdec($hex); - } - - $remain = substr($hex, 0, -1); - $last = substr($hex, -1); - return bcadd(bcmul(16, self::bchexdec($remain), 0), hexdec($last), 0); - } - - /** - * BC Math function to convert a DECIMAL string into a BINARY string - */ - public static function bcdecbin($dec, $pad = null) - { - $bin = ''; - while ($dec) { - $m = bcmod($dec, 2); - $dec = bcdiv($dec, 2, 0); - $bin = abs($m) . $bin; - } - return $pad ? sprintf("%0{$pad}s", $bin) : $bin; - } - - /** - * BC Math function to convert a BINARY string into a DECIMAL string - */ - public static function bcbindec($bin) - { - $dec = '0'; - for ($i=0, $j=strlen($bin); $i<$j; $i++) { - $dec = bcmul($dec, '2', 0); - $dec = bcadd($dec, $bin[$i], 0); - } - return $dec; - } - - /** - * BC Math function to convert a BINARY string into a HEX string - */ - public static function bcbinhex($bin, $pad = 0) - { - return self::bcdechex(self::bcbindec($bin)); - } - - /** - * BC Math function to convert a DECIMAL into a HEX string - */ - public static function bcdechex($dec) - { - $last = bcmod($dec, 16); - $remain = bcdiv(bcsub($dec, $last, 0), 16, 0); - return $remain == 0 ? dechex($last) : self::bcdechex($remain) . dechex($last); - } - - /** - * Bitwise AND two arbitrarily large numbers together. - */ - public static function bcand($left, $right) - { - $len = self::_bitwise($left, $right); - - $value = ''; - for ($i=0; $i<$len; $i++) { - $value .= (($left[$i] + 0) & ($right[$i] + 0)) ? '1' : '0'; - } - return self::bcbindec($value != '' ? $value : '0'); - } - - /** - * Bitwise OR two arbitrarily large numbers together. - */ - public static function bcor($left, $right) - { - $len = self::_bitwise($left, $right); - - $value = ''; - for ($i=0; $i<$len; $i++) { - $value .= (($left[$i] + 0) | ($right[$i] + 0)) ? '1' : '0'; - } - return self::bcbindec($value != '' ? $value : '0'); - } - - /** - * Bitwise XOR two arbitrarily large numbers together. - */ - public static function bcxor($left, $right) - { - $len = self::_bitwise($left, $right); - - $value = ''; - for ($i=0; $i<$len; $i++) { - $value .= (($left[$i] + 0) ^ ($right[$i] + 0)) ? '1' : '0'; - } - return self::bcbindec($value != '' ? $value : '0'); - } - - /** - * Bitwise NOT two arbitrarily large numbers together. - */ - public static function bcnot($left, $bits = null) - { - $right = 0; - $len = self::_bitwise($left, $right, $bits); - $value = ''; - for ($i=0; $i<$len; $i++) { - $value .= $left[$i] == '1' ? '0' : '1'; - } - return self::bcbindec($value); - } - - /** - * Shift number to the left - * - * @param integer $bits Total bits to shift - */ - public static function bcleft($num, $bits) { - return bcmul($num, bcpow('2', $bits)); - } - - /** - * Shift number to the right - * - * @param integer $bits Total bits to shift - */ - public static function bcright($num, $bits) { - return bcdiv($num, bcpow('2', $bits)); - } - - /** - * Determine how many bits are needed to store the number rounded to the - * nearest bit boundary. - */ - public static function bits_needed($num, $boundary = 4) - { - $bits = 0; - while ($num > 0) { - $num = bcdiv($num, '2', 0); - $bits++; - } - // round to nearest boundrary - return $boundary ? ceil($bits / $boundary) * $boundary : $bits; - } - - /** - * BC Math function to return an arbitrarily large random number. - */ - public static function bcrand($min, $max = null) - { - if ($max === null) { - $max = $min; - $min = 0; - } - - // swap values if $min > $max - if (bccomp($min, $max) == 1) { - list($min,$max) = array($max,$min); - } - - return bcadd( - bcmul( - bcdiv( - mt_rand(0, mt_getrandmax()), - mt_getrandmax(), - strlen($max) - ), - bcsub( - bcadd($max, '1'), - $min - ) - ), - $min - ); - } - - /** - * Computes the natural logarithm using a series. - * @author Thomas Oldbury. - * @license Public domain. - */ - public static function bclog($num, $iter = 10, $scale = 100) - { - $log = "0.0"; - for($i = 0; $i < $iter; $i++) { - $pow = 1 + (2 * $i); - $mul = bcdiv("1.0", $pow, $scale); - $fraction = bcmul($mul, bcpow(bcsub($num, "1.0", $scale) / bcadd($num, "1.0", $scale), $pow, $scale), $scale); - $log = bcadd($fraction, $log, $scale); - } - return bcmul("2.0", $log, $scale); - } - - /** - * Computes the base2 log using baseN log. - */ - public static function bclog2($num, $iter = 10, $scale = 100) - { - return bcdiv(self::bclog($num, $iter, $scale), self::bclog("2", $iter, $scale), $scale); - } - - public static function bcfloor($num) - { - if (substr($num, 0, 1) == '-') { - return bcsub($num, 1, 0); - } - return bcadd($num, 0, 0); - } - - public static function bcceil($num) - { - if (substr($num, 0, 1) == '-') { - return bcsub($num, 0, 0); - } - return bcadd($num, 1, 0); - } - - /** - * Compare two numbers and return -1, 0, 1 depending if the LEFT number is - * < = > the RIGHT. - * - * @param string|integer $left Left side operand - * @param string|integer $right Right side operand - * @return integer Return -1,0,1 for <=> comparison - */ - public static function cmp($left, $right) - { - // @todo could an optimization be done to determine if a normal 32bit - // comparison could be done instead of using bccomp? But would - // the number verification cause too much overhead to be useful? - return bccomp($left, $right, 0); - } - - /** - * Internal function to prepare for bitwise operations - */ - private static function _bitwise(&$left, &$right, $bits = null) - { - if ($bits === null) { - $bits = max(self::bits_needed($left), self::bits_needed($right)); - } - - $left = self::bcdecbin($left); - $right = self::bcdecbin($right); - - $len = max(strlen($left), strlen($right), (int)$bits); - - $left = sprintf("%0{$len}s", $left); - $right = sprintf("%0{$len}s", $right); - - return $len; - } - -} diff --git a/inc/lib/IP/Lifo/IP/CIDR.php b/inc/lib/IP/Lifo/IP/CIDR.php deleted file mode 100755 index e8fe32ce..00000000 --- a/inc/lib/IP/Lifo/IP/CIDR.php +++ /dev/null @@ -1,706 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace Lifo\IP; - -/** - * CIDR Block helper class. - * - * Most routines can be used statically or by instantiating an object and - * calling its methods. - * - * Provides routines to do various calculations on IP addresses and ranges. - * Convert to/from CIDR to ranges, etc. - */ -class CIDR -{ - const INTERSECT_NO = 0; - const INTERSECT_YES = 1; - const INTERSECT_LOW = 2; - const INTERSECT_HIGH = 3; - - protected $start; - protected $end; - protected $prefix; - protected $version; - protected $istart; - protected $iend; - - private $cache; - - /** - * Create a new CIDR object. - * - * The IP range can be arbitrary and does not have to fall on a valid CIDR - * range. Some methods will return different values depending if you ignore - * the prefix or not. By default all prefix sensitive methods will assume - * the prefix is used. - * - * @param string $cidr An IP address (1.2.3.4), CIDR block (1.2.3.4/24), - * or range "1.2.3.4-1.2.3.10" - * @param string $end Ending IP in range if no cidr/prefix is given - */ - public function __construct($cidr, $end = null) - { - if ($end !== null) { - $this->setRange($cidr, $end); - } else { - $this->setCidr($cidr); - } - } - - /** - * Returns the string representation of the CIDR block. - */ - public function __toString() - { - // do not include the prefix if its a single IP - try { - if ($this->isTrueCidr() && ( - ($this->version == 4 and $this->prefix != 32) || - ($this->version == 6 and $this->prefix != 128) - ) - ) { - return $this->start . '/' . $this->prefix; - } - } catch (\Exception $e) { - // isTrueCidr() calls getRange which can throw an exception - } - if (strcmp($this->start, $this->end) == 0) { - return $this->start; - } - return $this->start . ' - ' . $this->end; - } - - public function __clone() - { - // do not clone the cache. No real reason why. I just want to keep the - // memory foot print as low as possible, even though this is trivial. - $this->cache = array(); - } - - /** - * Set an arbitrary IP range. - * The closest matching prefix will be calculated but the actual range - * stored in the object can be arbitrary. - * @param string $start Starting IP or combination "start-end" string. - * @param string $end Ending IP or null. - */ - public function setRange($ip, $end = null) - { - if (strpos($ip, '-') !== false) { - list($ip, $end) = array_map('trim', explode('-', $ip, 2)); - } - - if (false === filter_var($ip, FILTER_VALIDATE_IP) || - false === filter_var($end, FILTER_VALIDATE_IP)) { - throw new \InvalidArgumentException("Invalid IP range \"$ip-$end\""); - } - - // determine version (4 or 6) - $this->version = (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ? 6 : 4; - - $this->istart = IP::inet_ptod($ip); - $this->iend = IP::inet_ptod($end); - - // fix order - if (bccomp($this->istart, $this->iend) == 1) { - list($this->istart, $this->iend) = array($this->iend, $this->istart); - list($ip, $end) = array($end, $ip); - } - - $this->start = $ip; - $this->end = $end; - - // calculate real prefix - $len = $this->version == 4 ? 32 : 128; - $this->prefix = $len - strlen(BC::bcdecbin(BC::bcxor($this->istart, $this->iend))); - } - - /** - * Returns true if the current IP is a true cidr block - */ - public function isTrueCidr() - { - return $this->start == $this->getNetwork() && $this->end == $this->getBroadcast(); - } - - /** - * Set the CIDR block. - * - * The prefix length is optional and will default to 32 ot 128 depending on - * the version detected. - * - * @param string $cidr CIDR block string, eg: "192.168.0.0/24" or "2001::1/64" - * @throws \InvalidArgumentException If the CIDR block is invalid - */ - public function setCidr($cidr) - { - if (strpos($cidr, '-') !== false) { - return $this->setRange($cidr); - } - - list($ip, $bits) = array_pad(array_map('trim', explode('/', $cidr, 2)), 2, null); - if (false === filter_var($ip, FILTER_VALIDATE_IP)) { - throw new \InvalidArgumentException("Invalid IP address \"$cidr\""); - } - - // determine version (4 or 6) - $this->version = (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ? 6 : 4; - - $this->start = $ip; - $this->istart = IP::inet_ptod($ip); - - if ($bits !== null and $bits !== '') { - $this->prefix = $bits; - } else { - $this->prefix = $this->version == 4 ? 32 : 128; - } - - if (($this->prefix < 0) - || ($this->prefix > 32 and $this->version == 4) - || ($this->prefix > 128 and $this->version == 6)) { - throw new \InvalidArgumentException("Invalid IP address \"$cidr\""); - } - - $this->end = $this->getBroadcast(); - $this->iend = IP::inet_ptod($this->end); - - $this->cache = array(); - } - - /** - * Get the IP version. 4 or 6. - * - * @return integer - */ - public function getVersion() - { - return $this->version; - } - - /** - * Get the prefix. - * - * Always returns the "proper" prefix, even if the IP range is arbitrary. - * - * @return integer - */ - public function getPrefix() - { - return $this->prefix; - } - - /** - * Return the starting presentational IP or Decimal value. - * - * Ignores prefix - */ - public function getStart($decimal = false) - { - return $decimal ? $this->istart : $this->start; - } - - /** - * Return the ending presentational IP or Decimal value. - * - * Ignores prefix - */ - public function getEnd($decimal = false) - { - return $decimal ? $this->iend : $this->end; - } - - /** - * Return the next presentational IP or Decimal value (following the - * broadcast address of the current CIDR block). - */ - public function getNext($decimal = false) - { - $next = bcadd($this->getEnd(true), '1'); - return $decimal ? $next : new self(IP::inet_dtop($next)); - } - - /** - * Returns true if the IP is an IPv4 - * - * @return boolean - */ - public function isIPv4() - { - return $this->version == 4; - } - - /** - * Returns true if the IP is an IPv6 - * - * @return boolean - */ - public function isIPv6() - { - return $this->version == 6; - } - - /** - * Get the cidr notation for the subnet block. - * - * This is useful for when you want a string representation of the IP/prefix - * and the starting IP is not on a valid network boundrary (eg: Displaying - * an IP from an interface). - * - * @return string IP in CIDR notation "ipaddr/prefix" - */ - public function getCidr() - { - return $this->start . '/' . $this->prefix; - } - - /** - * Get the [low,high] range of the CIDR block - * - * Prefix sensitive. - * - * @param boolean $ignorePrefix If true the arbitrary start-end range is - * returned. default=false. - */ - public function getRange($ignorePrefix = false) - { - $range = $ignorePrefix - ? array($this->start, $this->end) - : self::cidr_to_range($this->start, $this->prefix); - // watch out for IP '0' being converted to IPv6 '::' - if ($range[0] == '::' and strpos($range[1], ':') == false) { - $range[0] = '0.0.0.0'; - } - return $range; - } - - /** - * Return the IP in its fully expanded form. - * - * For example: 2001::1 == 2007:0000:0000:0000:0000:0000:0000:0001 - * - * @see IP::inet_expand - */ - public function getExpanded() - { - return IP::inet_expand($this->start); - } - - /** - * Get network IP of the CIDR block - * - * Prefix sensitive. - * - * @param boolean $ignorePrefix If true the arbitrary start-end range is - * returned. default=false. - */ - public function getNetwork($ignorePrefix = false) - { - // micro-optimization to prevent calling getRange repeatedly - $k = $ignorePrefix ? 1 : 0; - if (!isset($this->cache['range'][$k])) { - $this->cache['range'][$k] = $this->getRange($ignorePrefix); - } - return $this->cache['range'][$k][0]; - } - - /** - * Get broadcast IP of the CIDR block - * - * Prefix sensitive. - * - * @param boolean $ignorePrefix If true the arbitrary start-end range is - * returned. default=false. - */ - public function getBroadcast($ignorePrefix = false) - { - // micro-optimization to prevent calling getRange repeatedly - $k = $ignorePrefix ? 1 : 0; - if (!isset($this->cache['range'][$k])) { - $this->cache['range'][$k] = $this->getRange($ignorePrefix); - } - return $this->cache['range'][$k][1]; - } - - /** - * Get the network mask based on the prefix. - * - */ - public function getMask() - { - return self::prefix_to_mask($this->prefix, $this->version); - } - - /** - * Get total hosts within CIDR range - * - * Prefix sensitive. - * - * @param boolean $ignorePrefix If true the arbitrary start-end range is - * returned. default=false. - */ - public function getTotal($ignorePrefix = false) - { - // micro-optimization to prevent calling getRange repeatedly - $k = $ignorePrefix ? 1 : 0; - if (!isset($this->cache['range'][$k])) { - $this->cache['range'][$k] = $this->getRange($ignorePrefix); - } - return bcadd(bcsub(IP::inet_ptod($this->cache['range'][$k][1]), - IP::inet_ptod($this->cache['range'][$k][0])), '1'); - } - - public function intersects($cidr) - { - return self::cidr_intersect((string)$this, $cidr); - } - - /** - * Determines the intersection between an IP (with optional prefix) and a - * CIDR block. - * - * The IP will be checked against the CIDR block given and will either be - * inside or outside the CIDR completely, or partially. - * - * NOTE: The caller should explicitly check against the INTERSECT_* - * constants because this method will return a value > 1 even for partial - * matches. - * - * @param mixed $ip The IP/cidr to match - * @param mixed $cidr The CIDR block to match within - * @return integer Returns an INTERSECT_* constant - * @throws \InvalidArgumentException if either $ip or $cidr is invalid - */ - public static function cidr_intersect($ip, $cidr) - { - // use fixed length HEX strings so we can easily do STRING comparisons - // instead of using slower bccomp() math. - list($lo,$hi) = array_map(function($v){ return sprintf("%032s", IP::inet_ptoh($v)); }, CIDR::cidr_to_range($ip)); - list($min,$max) = array_map(function($v){ return sprintf("%032s", IP::inet_ptoh($v)); }, CIDR::cidr_to_range($cidr)); - - /** visualization of logic used below - lo-hi = $ip to check - min-max = $cidr block being checked against - --- --- --- lo --- --- hi --- --- --- --- --- IP/prefix to check - --- min --- --- max --- --- --- --- --- --- --- Partial "LOW" match - --- --- --- --- --- min --- --- max --- --- --- Partial "HIGH" match - --- --- --- --- min max --- --- --- --- --- --- No match "NO" - --- --- --- --- --- --- --- --- min --- max --- No match "NO" - min --- max --- --- --- --- --- --- --- --- --- No match "NO" - --- --- min --- --- --- --- max --- --- --- --- Full match "YES" - */ - - // IP is exact match or completely inside the CIDR block - if ($lo >= $min and $hi <= $max) { - return self::INTERSECT_YES; - } - - // IP is completely outside the CIDR block - if ($max < $lo or $min > $hi) { - return self::INTERSECT_NO; - } - - // @todo is it useful to return LOW/HIGH partial matches? - - // IP matches the lower end - if ($max <= $hi and $min <= $lo) { - return self::INTERSECT_LOW; - } - - // IP matches the higher end - if ($min >= $lo and $max >= $hi) { - return self::INTERSECT_HIGH; - } - - return self::INTERSECT_NO; - } - - /** - * Converts an IPv4 or IPv6 CIDR block into its range. - * - * @todo May not be the fastest way to do this. - * - * @static - * @param string $cidr CIDR block or IP address string. - * @param integer|null $bits If /bits is not specified on string they can be - * passed via this parameter instead. - * @return array A 2 element array with the low, high range - */ - public static function cidr_to_range($cidr, $bits = null) - { - if (strpos($cidr, '/') !== false) { - list($ip, $_bits) = array_pad(explode('/', $cidr, 2), 2, null); - } else { - $ip = $cidr; - $_bits = $bits; - } - - if (false === filter_var($ip, FILTER_VALIDATE_IP)) { - throw new \InvalidArgumentException("IP address \"$cidr\" is invalid"); - } - - // force bit length to 32 or 128 depending on type of IP - $bitlen = (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ? 128 : 32; - - if ($bits === null) { - // if no prefix is given use the length of the binary string which - // will give us 32 or 128 and result in a single IP being returned. - $bits = $_bits !== null ? $_bits : $bitlen; - } - - if ($bits > $bitlen) { - throw new \InvalidArgumentException("IP address \"$cidr\" is invalid"); - } - - $ipdec = IP::inet_ptod($ip); - $ipbin = BC::bcdecbin($ipdec, $bitlen); - - // calculate network - $netmask = BC::bcbindec(str_pad(str_repeat('1',$bits), $bitlen, '0')); - $ip1 = BC::bcand($ipdec, $netmask); - - // calculate "broadcast" (not technically a broadcast in IPv6) - $ip2 = BC::bcor($ip1, BC::bcnot($netmask)); - - return array(IP::inet_dtop($ip1), IP::inet_dtop($ip2)); - } - - /** - * Return the CIDR string from the range given - */ - public static function range_to_cidr($start, $end) - { - $cidr = new CIDR($start, $end); - return (string)$cidr; - } - - /** - * Return the maximum prefix length that would fit the IP address given. - * - * This is useful to determine how my bit would be needed to store the IP - * address when you don't already have a prefix for the IP. - * - * @example 216.240.32.0 would return 27 - * - * @param string $ip IP address without prefix - * @param integer $bits Maximum bits to check; defaults to 32 for IPv4 and 128 for IPv6 - */ - public static function max_prefix($ip, $bits = null) - { - static $mask = array(); - - $ver = (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ? 6 : 4; - $max = $ver == 6 ? 128 : 32; - if ($bits === null) { - $bits = $max; - - } - - $int = IP::inet_ptod($ip); - while ($bits > 0) { - // micro-optimization; calculate mask once ... - if (!isset($mask[$ver][$bits-1])) { - // 2^$max - 2^($max - $bits); - if ($ver == 4) { - $mask[$ver][$bits-1] = pow(2, $max) - pow(2, $max - ($bits-1)); - } else { - $mask[$ver][$bits-1] = bcsub(bcpow(2, $max), bcpow(2, $max - ($bits-1))); - } - } - - $m = $mask[$ver][$bits-1]; - //printf("%s/%d: %s & %s == %s\n", $ip, $bits-1, BC::bcdecbin($m, 32), BC::bcdecbin($int, 32), BC::bcdecbin(BC::bcand($int, $m))); - //echo "$ip/", $bits-1, ": ", IP::inet_dtop($m), " ($m) & $int == ", BC::bcand($int, $m), "\n"; - if (bccomp(BC::bcand($int, $m), $int) != 0) { - return $bits; - } - $bits--; - } - return $bits; - } - - /** - * Return a contiguous list of true CIDR blocks that span the range given. - * - * Note: It's not a good idea to call this with IPv6 addresses. While it may - * work for certain ranges this can be very slow. Also an IPv6 list won't be - * as accurate as an IPv4 list. - * - * @example - * range_to_cidrlist(192.168.0.0, 192.168.0.15) == - * 192.168.0.0/28 - * range_to_cidrlist(192.168.0.0, 192.168.0.20) == - * 192.168.0.0/28 - * 192.168.0.16/30 - * 192.168.0.20/32 - */ - public static function range_to_cidrlist($start, $end) - { - $ver = (false === filter_var($start, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ? 6 : 4; - $start = IP::inet_ptod($start); - $end = IP::inet_ptod($end); - - $len = $ver == 4 ? 32 : 128; - $log2 = $ver == 4 ? log(2) : BC::bclog(2); - - $list = array(); - while (BC::cmp($end, $start) >= 0) { // $end >= $start - $prefix = self::max_prefix(IP::inet_dtop($start), $len); - if ($ver == 4) { - $diff = $len - floor( log($end - $start + 1) / $log2 ); - } else { - // this is not as accurate due to the bclog function - $diff = bcsub($len, BC::bcfloor(bcdiv(BC::bclog(bcadd(bcsub($end, $start), '1')), $log2))); - } - - if ($prefix < $diff) { - $prefix = $diff; - } - - $list[] = IP::inet_dtop($start) . "/" . $prefix; - - if ($ver == 4) { - $start += pow(2, $len - $prefix); - } else { - $start = bcadd($start, bcpow(2, $len - $prefix)); - } - } - return $list; - } - - /** - * Return an list of optimized CIDR blocks by collapsing adjacent CIDR - * blocks into larger blocks. - * - * @param array $cidrs List of CIDR block strings or objects - * @param integer $maxPrefix Maximum prefix to allow - * @return array Optimized list of CIDR objects - */ - public static function optimize_cidrlist($cidrs, $maxPrefix = 32) - { - // all indexes must be a CIDR object - $cidrs = array_map(function($o){ return $o instanceof CIDR ? $o : new CIDR($o); }, $cidrs); - // sort CIDR blocks in proper order so we can easily loop over them - $cidrs = self::cidr_sort($cidrs); - - $list = array(); - while ($cidrs) { - $c = array_shift($cidrs); - $start = $c->getStart(); - - $max = bcadd($c->getStart(true), $c->getTotal()); - - // loop through each cidr block until its ending range is more than - // the current maximum. - while (!empty($cidrs) and $cidrs[0]->getStart(true) <= $max) { - $b = array_shift($cidrs); - $newmax = bcadd($b->getStart(true), $b->getTotal()); - if ($newmax > $max) { - $max = $newmax; - } - } - - // add the new cidr range to the optimized list - $list = array_merge($list, self::range_to_cidrlist($start, IP::inet_dtop(bcsub($max, '1')))); - } - - return $list; - } - - /** - * Sort the list of CIDR blocks, optionally with a custom callback function. - * - * @param array $cidrs A list of CIDR blocks (strings or objects) - * @param Closure $callback Optional callback to perform the sorting. - * See PHP usort documentation for more details. - */ - public static function cidr_sort($cidrs, $callback = null) - { - // all indexes must be a CIDR object - $cidrs = array_map(function($o){ return $o instanceof CIDR ? $o : new CIDR($o); }, $cidrs); - - if ($callback === null) { - $callback = function($a, $b) { - if (0 != ($o = BC::cmp($a->getStart(true), $b->getStart(true)))) { - return $o; // < or > - } - if ($a->getPrefix() == $b->getPrefix()) { - return 0; - } - return $a->getPrefix() < $b->getPrefix() ? -1 : 1; - }; - } elseif (!($callback instanceof \Closure) or !is_callable($callback)) { - throw new \InvalidArgumentException("Invalid callback in CIDR::cidr_sort, expected Closure, got " . gettype($callback)); - } - - usort($cidrs, $callback); - return $cidrs; - } - - /** - * Return the Prefix bits from the IPv4 mask given. - * - * This is only valid for IPv4 addresses since IPv6 addressing does not - * have a concept of network masks. - * - * Example: 255.255.255.0 == 24 - * - * @param string $mask IPv4 network mask. - */ - public static function mask_to_prefix($mask) - { - if (false === filter_var($mask, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { - throw new \InvalidArgumentException("Invalid IP netmask \"$mask\""); - } - return strrpos(IP::inet_ptob($mask, 32), '1') + 1; - } - - /** - * Return the network mask for the prefix given. - * - * Normally this is only useful for IPv4 addresses but you can generate a - * mask for IPv6 addresses as well, only because its mathematically - * possible. - * - * @param integer $prefix CIDR prefix bits (0-128) - * @param integer $version IP version. If null the version will be detected - * based on the prefix length given. - */ - public static function prefix_to_mask($prefix, $version = null) - { - if ($version === null) { - $version = $prefix > 32 ? 6 : 4; - } - if ($prefix < 0 or $prefix > 128) { - throw new \InvalidArgumentException("Invalid prefix length \"$prefix\""); - } - if ($version != 4 and $version != 6) { - throw new \InvalidArgumentException("Invalid version \"$version\". Must be 4 or 6"); - } - - if ($version == 4) { - return long2ip($prefix == 0 ? 0 : (0xFFFFFFFF >> (32 - $prefix)) << (32 - $prefix)); - } else { - return IP::inet_dtop($prefix == 0 ? 0 : BC::bcleft(BC::bcright(BC::MAX_UINT_128, 128-$prefix), 128-$prefix)); - } - } - - /** - * Return true if the $ip given is a true CIDR block. - * - * A true CIDR block is one where the $ip given is the actual Network - * address and broadcast matches the prefix appropriately. - */ - public static function cidr_is_true($ip) - { - $ip = new CIDR($ip); - return $ip->isTrueCidr(); - } -} diff --git a/inc/lib/IP/Lifo/IP/IP.php b/inc/lib/IP/Lifo/IP/IP.php deleted file mode 100755 index 4d22aa76..00000000 --- a/inc/lib/IP/Lifo/IP/IP.php +++ /dev/null @@ -1,207 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace Lifo\IP; - -/** - * IP Address helper class. - * - * Provides routines to translate IPv4 and IPv6 addresses between human readable - * strings, decimal, hexidecimal and binary. - * - * Requires BCmath extension and IPv6 PHP support - */ -abstract class IP -{ - /** - * Convert a human readable (presentational) IP address string into a decimal string. - */ - public static function inet_ptod($ip) - { - // shortcut for IPv4 addresses - if (strpos($ip, ':') === false && strpos($ip, '.') !== false) { - return sprintf('%u', ip2long($ip)); - } - - // remove any cidr block notation - if (($o = strpos($ip, '/')) !== false) { - $ip = substr($ip, 0, $o); - } - - // unpack into 4 32bit integers - $parts = unpack('N*', inet_pton($ip)); - foreach ($parts as &$part) { - if ($part < 0) { - // convert signed int into unsigned - $part = sprintf('%u', $part); - //$part = bcadd($part, '4294967296'); - } - } - - // add each 32bit integer to the proper bit location in our big decimal - $decimal = $parts[4]; // << 0 - $decimal = bcadd($decimal, bcmul($parts[3], '4294967296')); // << 32 - $decimal = bcadd($decimal, bcmul($parts[2], '18446744073709551616')); // << 64 - $decimal = bcadd($decimal, bcmul($parts[1], '79228162514264337593543950336')); // << 96 - - return $decimal; - } - - /** - * Convert a decimal string into a human readable IP address. - */ - public static function inet_dtop($decimal, $expand = false) - { - $parts = array(); - $parts[1] = bcdiv($decimal, '79228162514264337593543950336', 0); // >> 96 - $decimal = bcsub($decimal, bcmul($parts[1], '79228162514264337593543950336')); - $parts[2] = bcdiv($decimal, '18446744073709551616', 0); // >> 64 - $decimal = bcsub($decimal, bcmul($parts[2], '18446744073709551616')); - $parts[3] = bcdiv($decimal, '4294967296', 0); // >> 32 - $decimal = bcsub($decimal, bcmul($parts[3], '4294967296')); - $parts[4] = $decimal; // >> 0 - - foreach ($parts as &$part) { - if (bccomp($part, '2147483647') == 1) { - $part = bcsub($part, '4294967296'); - } - $part = (int) $part; - } - - // if the first 96bits is all zeros then we can safely assume we - // actually have an IPv4 address. Even though it's technically possible - // you're not really ever going to see an IPv6 address in the range: - // ::0 - ::ffff - // It's feasible to see an IPv6 address of "::", in which case the - // caller is going to have to account for that on their own. - if (($parts[1] | $parts[2] | $parts[3]) == 0) { - $ip = long2ip($parts[4]); - } else { - $packed = pack('N4', $parts[1], $parts[2], $parts[3], $parts[4]); - $ip = inet_ntop($packed); - } - - // Turn IPv6 to IPv4 if it's IPv4 - if (preg_match('/^::\d+\./', $ip)) { - return substr($ip, 2); - } - - return $expand ? self::inet_expand($ip) : $ip; - } - - /** - * Convert a human readable (presentational) IP address into a HEX string. - */ - public static function inet_ptoh($ip) - { - return bin2hex(inet_pton($ip)); - //return BC::bcdechex(self::inet_ptod($ip)); - } - - /** - * Convert a human readable (presentational) IP address into a BINARY string. - */ - public static function inet_ptob($ip, $bits = 128) - { - return BC::bcdecbin(self::inet_ptod($ip), $bits); - } - - /** - * Convert a binary string into an IP address (presentational) string. - */ - public static function inet_btop($bin) - { - return self::inet_dtop(BC::bcbindec($bin)); - } - - /** - * Convert a HEX string into a human readable (presentational) IP address - */ - public static function inet_htop($hex) - { - return self::inet_dtop(BC::bchexdec($hex)); - } - - /** - * Expand an IP address. IPv4 addresses are returned as-is. - * - * Example: - * 2001::1 expands to 2001:0000:0000:0000:0000:0000:0000:0001 - * ::127.0.0.1 expands to 0000:0000:0000:0000:0000:0000:7f00:0001 - * 127.0.0.1 expands to 127.0.0.1 - */ - public static function inet_expand($ip) - { - // strip possible cidr notation off - if (($pos = strpos($ip, '/')) !== false) { - $ip = substr($ip, 0, $pos); - } - $bytes = unpack('n*', inet_pton($ip)); - if (count($bytes) > 2) { - return implode(':', array_map(function ($b) { - return sprintf("%04x", $b); - }, $bytes)); - } - return $ip; - } - - /** - * Convert an IPv4 address into an IPv6 address. - * - * One use-case for this is IP 6to4 tunnels used in networking. - * - * @example - * to_ipv4("10.10.10.10") == a0a:a0a - * - * @param string $ip IPv4 address. - * @param boolean $mapped If true a Full IPv6 address is returned within the - * official ipv4to6 mapped space "0:0:0:0:0:ffff:x:x" - */ - public static function to_ipv6($ip, $mapped = false) - { - if (!self::isIPv4($ip)) { - throw new \InvalidArgumentException("Invalid IPv4 address \"$ip\""); - } - - $num = IP::inet_ptod($ip); - $o1 = dechex($num >> 16); - $o2 = dechex($num & 0x0000FFFF); - - return $mapped ? "0:0:0:0:0:ffff:$o1:$o2" : "$o1:$o2"; - } - - /** - * Returns true if the IP address is a valid IPv4 address - */ - public static function isIPv4($ip) - { - return $ip === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); - } - - /** - * Returns true if the IP address is a valid IPv6 address - */ - public static function isIPv6($ip) - { - return $ip === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6); - } - - /** - * Compare two IP's (v4 or v6) and return -1, 0, 1 if the first is < = > - * the second. - * - * @param string $ip1 IP address - * @param string $ip2 IP address to compare against - * @return integer Return -1,0,1 depending if $ip1 is <=> $ip2 - */ - public static function cmp($ip1, $ip2) - { - return bccomp(self::inet_ptod($ip1), self::inet_ptod($ip2), 0); - } -} From 0205ea2da68e8e523568b34629b9460936af60fb Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 18 Dec 2024 15:07:40 +0100 Subject: [PATCH 009/258] Track banners --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index 93cac6d4..3205c64b 100644 --- a/.gitignore +++ b/.gitignore @@ -70,9 +70,6 @@ tf/ /mod/ /random/ -# Banners -static/banners/* - #Fonts stylesheets/fonts From dbb44dfa91eda3a9fdaabb74b70c5c2bb7db144a Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 18 Dec 2024 15:10:21 +0100 Subject: [PATCH 010/258] banners: add deny defend depose banner --- static/banners/deny-defend-depose-opt.webp | Bin 0 -> 5734 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 static/banners/deny-defend-depose-opt.webp diff --git a/static/banners/deny-defend-depose-opt.webp b/static/banners/deny-defend-depose-opt.webp new file mode 100644 index 0000000000000000000000000000000000000000..584a4ae20678e610b8da4efbbc42852215ff9e89 GIT binary patch literal 5734 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 literal 0 HcmV?d00001 From 6bcf22aa7ed5d341b8f6201977df5d55fc5fdc82 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 18 Dec 2024 15:10:44 +0100 Subject: [PATCH 011/258] banners: add just monika banner --- static/banners/just-monika-opt.webp | Bin 0 -> 26620 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 static/banners/just-monika-opt.webp diff --git a/static/banners/just-monika-opt.webp b/static/banners/just-monika-opt.webp new file mode 100644 index 0000000000000000000000000000000000000000..731f985a228069545967c42ed4b0a25d131e2307 GIT binary patch literal 26620 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 n- Date: Thu, 20 Jun 2024 10:32:12 -0300 Subject: [PATCH 012/258] posts.sql: update column password --- templates/posts.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/posts.sql b/templates/posts.sql index 9c468c97..71bad994 100755 --- a/templates/posts.sql +++ b/templates/posts.sql @@ -13,7 +13,7 @@ CREATE TABLE IF NOT EXISTS ``posts_{{ board }}`` ( `files` text DEFAULT NULL, `num_files` int(11) DEFAULT 0, `filehash` text CHARACTER SET ascii, - `password` varchar(20) DEFAULT NULL, + `password` varchar(64) DEFAULT NULL, `ip` varchar(39) CHARACTER SET ascii NOT NULL, `sticky` int(1) NOT NULL, `locked` int(1) NOT NULL, From 8b2f0025823edf6c2e03499fef091c4df04da310 Mon Sep 17 00:00:00 2001 From: fowr <89118232+perdedora@users.noreply.github.com> Date: Thu, 20 Jun 2024 10:11:47 -0300 Subject: [PATCH 013/258] hash poster passwords --- inc/config.php | 3 +++ inc/functions.php | 5 +++++ install.php | 1 + post.php | 9 ++++----- templates/installer/config.html | 3 +++ tools/hash-passwords.php | 17 +++++++++++++++++ 6 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 tools/hash-passwords.php diff --git a/inc/config.php b/inc/config.php index 633cdd33..08b01ead 100644 --- a/inc/config.php +++ b/inc/config.php @@ -200,6 +200,9 @@ // Used to salt secure tripcodes ("##trip") and poster IDs (if enabled). $config['secure_trip_salt'] = ')(*&^%$#@!98765432190zyxwvutsrqponmlkjihgfedcba'; + // Used to salt poster passwords. + $config['secure_password_salt'] = 'wKJSb7M5SyzMcFWD2gPO3j2RYUSO9B789!@#$%^&*()'; + /* * ==================== * Flood/spam settings diff --git a/inc/functions.php b/inc/functions.php index 767dfc06..66cd6fa7 100644 --- a/inc/functions.php +++ b/inc/functions.php @@ -3082,3 +3082,8 @@ function strategy_first($fun, $array) { return array('defer'); } } + +function hashPassword($password) { + global $config; + return hash('sha3-256', $password . $config['secure_password_salt']); +} diff --git a/install.php b/install.php index bc3ba7d4..7663a503 100644 --- a/install.php +++ b/install.php @@ -881,6 +881,7 @@ if ($step == 0) { $config['cookies']['salt'] = substr(base64_encode(sha1(rand())), 0, 30); $config['secure_trip_salt'] = substr(base64_encode(sha1(rand())), 0, 30); + $config['secure_password_salt'] = substr(base64_encode(sha1(rand())), 0, 30); echo Element('page.html', array( 'body' => Element('installer/config.html', array( diff --git a/post.php b/post.php index 642e8057..fa069370 100644 --- a/post.php +++ b/post.php @@ -530,10 +530,12 @@ function handle_delete(Context $ctx) $password = &$_POST['password']; - if ($password == '') { + if (empty($password)) { error($config['error']['invalidpassword']); } + $password = hashPassword($_POST['password']); + $delete = []; foreach ($_POST as $post => $value) { if (preg_match('/^delete_(\d+)$/', $post, $m)) { @@ -1013,7 +1015,7 @@ function handle_post(Context $ctx) $post['subject'] = $_POST['subject']; $post['email'] = str_replace(' ', '%20', htmlspecialchars($_POST['email'])); $post['body'] = $_POST['body']; - $post['password'] = $_POST['password']; + $post['password'] = hashPassword($_POST['password']); $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 (!$dropped_post) { @@ -1204,9 +1206,6 @@ function handle_post(Context $ctx) error($config['error']['toolong_body']); } } - if (mb_strlen($post['password']) > 20) { - error(sprintf($config['error']['toolong'], 'password')); - } } wordfilters($post['body']); diff --git a/templates/installer/config.html b/templates/installer/config.html index 973328f5..00a5b241 100644 --- a/templates/installer/config.html +++ b/templates/installer/config.html @@ -88,6 +88,9 @@ + + + diff --git a/tools/hash-passwords.php b/tools/hash-passwords.php new file mode 100644 index 00000000..3c6463ee --- /dev/null +++ b/tools/hash-passwords.php @@ -0,0 +1,17 @@ +execute() or error(db_error($query)); + + while($entry = $query->fetch(PDO::FETCH_ASSOC)) { + $update_query = prepare(sprintf("UPDATE ``posts_%s`` SET `password` = :password WHERE `password` = :password_org", $_board['uri'])); + $update_query->bindValue(':password', hashPassword($entry['password'])); + $update_query->bindValue(':password_org', $entry['password']); + $update_query->execute() or error(db_error()); + } + } From 57324f169de49ba3a870dd9d8e33466c5d4c6442 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Fri, 27 Dec 2024 15:29:02 +0100 Subject: [PATCH 014/258] post.php: restore password length limit --- post.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/post.php b/post.php index fa069370..0a7959c2 100644 --- a/post.php +++ b/post.php @@ -1011,6 +1011,11 @@ function handle_post(Context $ctx) } } + // We must do this check now before the passowrd is hashed and overwritten. + if (\mb_strlen($_POST['password']) > 20) { + error(\sprintf($config['error']['toolong'], 'password')); + } + $post['name'] = $_POST['name'] != '' ? $_POST['name'] : $config['anonymous']; $post['subject'] = $_POST['subject']; $post['email'] = str_replace(' ', '%20', htmlspecialchars($_POST['email'])); From de75d12bc5242654ae8410e8e15765bdd4356c95 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Fri, 27 Dec 2024 15:35:02 +0100 Subject: [PATCH 015/258] hash-passwords.php: minor refactor --- tools/hash-passwords.php | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/tools/hash-passwords.php b/tools/hash-passwords.php index 3c6463ee..0002bd70 100644 --- a/tools/hash-passwords.php +++ b/tools/hash-passwords.php @@ -2,16 +2,15 @@ require_once dirname(__FILE__) . '/inc/cli.php'; -$boards = listBoards(); - foreach ($boards as &$_board) { - query(sprintf('ALTER TABLE ``posts_%s`` MODIFY `password` varchar(64) DEFAULT NULL;', $_board['uri'])) or error(db_error()); - $query = prepare(sprintf("SELECT DISTINCT `password` FROM ``posts_%s``", $_board['uri'])); - $query->execute() or error(db_error($query)); +foreach (listBoards(true) as $uri) { + query(\sprintf('ALTER TABLE ``posts_%s`` MODIFY `password` varchar(64) DEFAULT NULL;', $uri)) or error(db_error()); + $query = prepare(\sprintf("SELECT DISTINCT `password` FROM ``posts_%s``", $uri)); + $query->execute() or error(db_error($query)); - while($entry = $query->fetch(PDO::FETCH_ASSOC)) { - $update_query = prepare(sprintf("UPDATE ``posts_%s`` SET `password` = :password WHERE `password` = :password_org", $_board['uri'])); - $update_query->bindValue(':password', hashPassword($entry['password'])); - $update_query->bindValue(':password_org', $entry['password']); - $update_query->execute() or error(db_error()); - } - } + while($entry = $query->fetch(\PDO::FETCH_ASSOC)) { + $update_query = prepare(\sprintf("UPDATE ``posts_%s`` SET `password` = :password WHERE `password` = :password_org", $uri)); + $update_query->bindValue(':password', hashPassword($entry['password'])); + $update_query->bindValue(':password_org', $entry['password']); + $update_query->execute() or error(db_error()); + } +} From 750ac11581c4f462a73418c553bbce1379dc1fe6 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 18 Dec 2024 23:03:16 +0100 Subject: [PATCH 016/258] Tinyboard.php: trim --- inc/lib/twig/extensions/Extension/Tinyboard.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/inc/lib/twig/extensions/Extension/Tinyboard.php b/inc/lib/twig/extensions/Extension/Tinyboard.php index 97fecb20..5f80dfc0 100644 --- a/inc/lib/twig/extensions/Extension/Tinyboard.php +++ b/inc/lib/twig/extensions/Extension/Tinyboard.php @@ -32,7 +32,7 @@ class Twig_Extensions_Extension_Tinyboard extends Twig_Extension new Twig_SimpleFilter('addslashes', 'addslashes'), ); } - + /** * Returns a list of functions to add to the existing list. * @@ -52,7 +52,7 @@ class Twig_Extensions_Extension_Tinyboard extends Twig_Extension new Twig_SimpleFunction('link_for', 'link_for') ); } - + /** * Returns the name of the extension. * @@ -88,7 +88,7 @@ function twig_hasPermission_filter($mod, $permission, $board = null) { function twig_extension_filter($value, $case_insensitive = true) { $ext = mb_substr($value, mb_strrpos($value, '.') + 1); if($case_insensitive) - $ext = mb_strtolower($ext); + $ext = mb_strtolower($ext); return $ext; } @@ -113,7 +113,7 @@ function twig_filename_truncate_filter($value, $length = 30, $separator = '…') $value = strrev($value); $array = array_reverse(explode(".", $value, 2)); $array = array_map("strrev", $array); - + $filename = &$array[0]; $extension = isset($array[1]) ? $array[1] : false; @@ -127,11 +127,13 @@ function twig_filename_truncate_filter($value, $length = 30, $separator = '…') function twig_ratio_function($w, $h) { return fraction($w, $h, ':'); } + function twig_secure_link_confirm($text, $title, $confirm_message, $href) { global $config; return '' . $text . ''; } + function twig_secure_link($href) { return $href . '/' . make_secure_link_token($href); } From 4f7d872d0d58a9d8c2e6ed4134ac438055f73eb8 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Fri, 27 Dec 2024 19:02:36 +0100 Subject: [PATCH 017/258] Tinyboard.php: remove dead code --- inc/lib/twig/extensions/Extension/Tinyboard.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/inc/lib/twig/extensions/Extension/Tinyboard.php b/inc/lib/twig/extensions/Extension/Tinyboard.php index 5f80dfc0..5fb99b11 100644 --- a/inc/lib/twig/extensions/Extension/Tinyboard.php +++ b/inc/lib/twig/extensions/Extension/Tinyboard.php @@ -129,8 +129,6 @@ function twig_ratio_function($w, $h) { } function twig_secure_link_confirm($text, $title, $confirm_message, $href) { - global $config; - return '' . $text . ''; } From bebe786c5bafd0ac7aadb6932e7bf53a7cad0465 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Fri, 27 Dec 2024 18:51:35 +0100 Subject: [PATCH 018/258] mod.php: dots are not required in the cursors --- mod.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mod.php b/mod.php index bb260251..554e4fd5 100644 --- a/mod.php +++ b/mod.php @@ -68,7 +68,7 @@ $pages = [ '/reports/(\d+)/dismiss(all)?' => 'secure report_dismiss', // dismiss a report '/IP/([\w.:]+)' => 'secure_POST ip', // view ip address - '/IP/([\w.:]+)/cursor/([\w|-|_|.]+)' => 'secure_POST ip', // view ip address + '/IP/([\w.:]+)/cursor/([\w|-|_]+)' => 'secure_POST ip', // view ip address '/IP/([\w.:]+)/remove_note/(\d+)' => 'secure ip_remove_note', // remove note from ip address '/ban' => 'secure_POST ban', // new ban From 4ede43b192575011071c50b035c7cd261100d0aa Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 18 Dec 2024 19:51:41 +0100 Subject: [PATCH 019/258] UserPostQueries.php: refactor implementation --- inc/Data/UserPostQueries.php | 80 +++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 37 deletions(-) diff --git a/inc/Data/UserPostQueries.php b/inc/Data/UserPostQueries.php index e702dd89..bebbd365 100644 --- a/inc/Data/UserPostQueries.php +++ b/inc/Data/UserPostQueries.php @@ -17,16 +17,7 @@ class UserPostQueries { $this->pdo = $pdo; } - /** - * Fetch a page of user posts. - * - * @param array $board_uris The uris of the boards that should be included. - * @param string $ip The IP of the target user. - * @param integer $page_size The Number of posts that should be fetched. - * @param string|null $cursor The directional cursor to fetch the next or previous page. Null to start from the beginning. - * @return PageFetchResult - */ - public function fetchPaginatedByIp(array $board_uris, string $ip, int $page_size, ?string $cursor = null): PageFetchResult { + private function paginate(array $board_uris, int $page_size, ?string $cursor, callable $callback): PageFetchResult { // Decode the cursor. if ($cursor !== null) { list($cursor_type, $uri_id_cursor_map) = Net\decode_cursor($cursor); @@ -41,37 +32,15 @@ class UserPostQueries { foreach ($board_uris as $uri) { // Extract the cursor relative to the board. - $id_cursor = false; - if (isset($uri_id_cursor_map[$uri])) { + $start_id = null; + if ($cursor_type !== null && isset($uri_id_cursor_map[$uri])) { $value = $uri_id_cursor_map[$uri]; if (\is_numeric($value)) { - $id_cursor = (int)$value; + $start_id = (int)$value; } } - if ($id_cursor === false) { - $query = $this->pdo->prepare(sprintf('SELECT * FROM `posts_%s` WHERE `ip` = :ip ORDER BY `sticky` DESC, `id` DESC LIMIT :limit', $uri)); - $query->bindValue(':ip', $ip); - $query->bindValue(':limit', $page_size + 1, \PDO::PARAM_INT); // Always fetch more. - $query->execute(); - $posts = $query->fetchAll(\PDO::FETCH_ASSOC); - } elseif ($cursor_type === self::CURSOR_TYPE_NEXT) { - $query = $this->pdo->prepare(sprintf('SELECT * FROM `posts_%s` WHERE `ip` = :ip AND `id` <= :start_id ORDER BY `sticky` DESC, `id` DESC LIMIT :limit', $uri)); - $query->bindValue(':ip', $ip); - $query->bindValue(':start_id', $id_cursor, \PDO::PARAM_INT); - $query->bindValue(':limit', $page_size + 2, \PDO::PARAM_INT); // Always fetch more. - $query->execute(); - $posts = $query->fetchAll(\PDO::FETCH_ASSOC); - } elseif ($cursor_type === self::CURSOR_TYPE_PREV) { - $query = $this->pdo->prepare(sprintf('SELECT * FROM `posts_%s` WHERE `ip` = :ip AND `id` >= :start_id ORDER BY `sticky` ASC, `id` ASC LIMIT :limit', $uri)); - $query->bindValue(':ip', $ip); - $query->bindValue(':start_id', $id_cursor, \PDO::PARAM_INT); - $query->bindValue(':limit', $page_size + 2, \PDO::PARAM_INT); // Always fetch more. - $query->execute(); - $posts = \array_reverse($query->fetchAll(\PDO::FETCH_ASSOC)); - } else { - throw new \RuntimeException("Unknown cursor type '$cursor_type'"); - } + $posts = $callback($uri, $cursor_type, $start_id, $page_size); $posts_count = \count($posts); @@ -79,7 +48,7 @@ class UserPostQueries { $has_extra_prev_post = true; $has_extra_end_post = true; } elseif ($posts_count === $page_size + 1) { - $has_extra_prev_post = $id_cursor !== false && $id_cursor === (int)$posts[0]['id']; + $has_extra_prev_post = $start_id !== null && $start_id === (int)$posts[0]['id']; $has_extra_end_post = !$has_extra_prev_post; } else { $has_extra_prev_post = false; @@ -112,4 +81,41 @@ class UserPostQueries { return $res; } + + /** + * Fetch a page of user posts. + * + * @param array $board_uris The uris of the boards that should be included. + * @param string $ip The IP of the target user. + * @param integer $page_size The Number of posts that should be fetched. + * @param string|null $cursor The directional cursor to fetch the next or previous page. Null to start from the beginning. + * @return PageFetchResult + */ + public function fetchPaginatedByIp(array $board_uris, string $ip, int $page_size, ?string $cursor = null): PageFetchResult { + return $this->paginate($board_uris, $page_size, $cursor, function($uri, $cursor_type, $start_id, $page_size) use ($ip) { + if ($cursor_type === null) { + $query = $this->pdo->prepare(sprintf('SELECT * FROM `posts_%s` WHERE `ip` = :ip ORDER BY `sticky` DESC, `id` DESC LIMIT :limit', $uri)); + $query->bindValue(':ip', $ip); + $query->bindValue(':limit', $page_size + 1, \PDO::PARAM_INT); // Always fetch more. + $query->execute(); + return $query->fetchAll(\PDO::FETCH_ASSOC); + } elseif ($cursor_type === self::CURSOR_TYPE_NEXT) { + $query = $this->pdo->prepare(sprintf('SELECT * FROM `posts_%s` WHERE `ip` = :ip AND `id` <= :start_id ORDER BY `sticky` DESC, `id` DESC LIMIT :limit', $uri)); + $query->bindValue(':ip', $ip); + $query->bindValue(':start_id', $start_id, \PDO::PARAM_INT); + $query->bindValue(':limit', $page_size + 2, \PDO::PARAM_INT); // Always fetch more. + $query->execute(); + return $query->fetchAll(\PDO::FETCH_ASSOC); + } elseif ($cursor_type === self::CURSOR_TYPE_PREV) { + $query = $this->pdo->prepare(sprintf('SELECT * FROM `posts_%s` WHERE `ip` = :ip AND `id` >= :start_id ORDER BY `sticky` ASC, `id` ASC LIMIT :limit', $uri)); + $query->bindValue(':ip', $ip); + $query->bindValue(':start_id', $start_id, \PDO::PARAM_INT); + $query->bindValue(':limit', $page_size + 2, \PDO::PARAM_INT); // Always fetch more. + $query->execute(); + return \array_reverse($query->fetchAll(\PDO::FETCH_ASSOC)); + } else { + throw new \RuntimeException("Unknown cursor type '$cursor_type'"); + } + }); + } } \ No newline at end of file From 27b7f1c60d12858f5c0bb35ef003e597c3110c71 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 18 Dec 2024 21:09:41 +0100 Subject: [PATCH 020/258] UserPostQueries.php: add post fetching by password --- inc/Data/UserPostQueries.php | 39 +++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/inc/Data/UserPostQueries.php b/inc/Data/UserPostQueries.php index bebbd365..ee16b2f5 100644 --- a/inc/Data/UserPostQueries.php +++ b/inc/Data/UserPostQueries.php @@ -118,4 +118,41 @@ class UserPostQueries { } }); } -} \ No newline at end of file + + /** + * Fetch a page of user posts. + * + * @param array $board_uris The uris of the boards that should be included. + * @param string $password The password of the target user. + * @param integer $page_size The Number of posts that should be fetched. + * @param string|null $cursor The directional cursor to fetch the next or previous page. Null to start from the beginning. + * @return PageFetchResult + */ + public function fetchPaginateByPassword(array $board_uris, string $password, int $page_size, ?string $cursor = null): PageFetchResult { + return $this->paginate($board_uris, $page_size, $cursor, function($uri, $cursor_type, $start_id, $page_size) use ($password) { + if ($cursor_type === null) { + $query = $this->pdo->prepare(sprintf('SELECT * FROM `posts_%s` WHERE `password` = :password ORDER BY `sticky` DESC, `id` DESC LIMIT :limit', $uri)); + $query->bindValue(':password', $password); + $query->bindValue(':limit', $page_size + 1, \PDO::PARAM_INT); // Always fetch more. + $query->execute(); + return $query->fetchAll(\PDO::FETCH_ASSOC); + } elseif ($cursor_type === self::CURSOR_TYPE_NEXT) { + $query = $this->pdo->prepare(sprintf('SELECT * FROM `posts_%s` WHERE `password` = :password AND `id` <= :start_id ORDER BY `sticky` DESC, `id` DESC LIMIT :limit', $uri)); + $query->bindValue(':password', $password); + $query->bindValue(':start_id', $start_id, \PDO::PARAM_INT); + $query->bindValue(':limit', $page_size + 2, \PDO::PARAM_INT); // Always fetch more. + $query->execute(); + return $query->fetchAll(\PDO::FETCH_ASSOC); + } elseif ($cursor_type === self::CURSOR_TYPE_PREV) { + $query = $this->pdo->prepare(sprintf('SELECT * FROM `posts_%s` WHERE `password` = :ip AND `password` >= :start_id ORDER BY `sticky` ASC, `id` ASC LIMIT :limit', $uri)); + $query->bindValue(':password', $password); + $query->bindValue(':start_id', $start_id, \PDO::PARAM_INT); + $query->bindValue(':limit', $page_size + 2, \PDO::PARAM_INT); // Always fetch more. + $query->execute(); + return \array_reverse($query->fetchAll(\PDO::FETCH_ASSOC)); + } else { + throw new \RuntimeException("Unknown cursor type '$cursor_type'"); + } + }); + } +} From 11c574888812b7add000ea34ae49977462adcf31 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 18 Dec 2024 22:10:08 +0100 Subject: [PATCH 021/258] pages.php: refactor mod_ip into mod_user_posts_by_ip --- inc/mod/pages.php | 137 +++++++++++++++++++++++++++------------------- 1 file changed, 80 insertions(+), 57 deletions(-) diff --git a/inc/mod/pages.php b/inc/mod/pages.php index 37b282c4..a057ad25 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -861,9 +861,9 @@ function mod_ip_remove_note(Context $ctx, $ip, $id) { $query->bindValue(':id', $id); $query->execute() or error(db_error($query)); - modLog("Removed a note for {$ip}"); + modLog("Removed a note for {$ip}"); - header('Location: ?/IP/' . $ip . '#notes', true, $config['redirect_http']); + \header("Location: ?/user_posts/ip/$ip#notes", true, $config['redirect_http']); } function mod_ip(Context $ctx, $ip, string $encoded_cursor = null) { @@ -879,9 +879,9 @@ function mod_ip(Context $ctx, $ip, string $encoded_cursor = null) { Bans::delete($_POST['ban_id'], true, $mod['boards']); if (empty($encoded_cursor)) { - header("Location: ?/IP/$ip#bans", true, $config['redirect_http']); + \header("Location: ?/user_posts/ip/$ip#bans", true, $config['redirect_http']); } else { - header("Location: ?/IP/$ip/cursor/$encoded_cursor#bans", true, $config['redirect_http']); + \header("Location: ?/user_posts/ip/$ip/cursor/$encoded_cursor#bans", true, $config['redirect_http']); } return; } @@ -901,64 +901,48 @@ function mod_ip(Context $ctx, $ip, string $encoded_cursor = null) { Cache::delete("mod_page_ip_view_notes_$ip"); - modLog("Added a note for {$ip}"); + modLog("Added a note for {$ip}"); if (empty($encoded_cursor)) { - header("Location: ?/IP/$ip#notes", true, $config['redirect_http']); + \header("Location: ?/user_posts/ip/$ip#notes", true, $config['redirect_http']); } else { - header("Location: ?/IP/$ip/cursor/$encoded_cursor#notes", true, $config['redirect_http']); + \header("Location: ?/user_posts/ip/$ip/cursor/$encoded_cursor#notes", true, $config['redirect_http']); } return; } + if (empty($encoded_cursor)) { + \header("Location: ?/user_posts/ip/$ip", true, 308); + } else { + \header("Location: ?/user_posts/ip/$ip/cursor/$encoded_cursor", true, 308); + } +} + +function mod_user_posts_by_ip(Context $ctx, string $ip, string $encoded_cursor = null) { + global $mod; + + if (\filter_var($ip, \FILTER_VALIDATE_IP) === false){ + error("Invalid IP address."); + } + + $config = $ctx->get('config'); + $args = [ 'ip' => $ip, 'posts' => [] ]; + if (isset($config['mod']['ip_recentposts'])) { + // TODO log to migrate. + $page_size = $config['mod']['ip_recentposts']; + } else { + $page_size = $config['mod']['recent_user_posts']; + } + if ($config['mod']['dns_lookup']) { $args['hostname'] = rDNS($ip); } - $boards = listBoards(); - - $queryable_uris = []; - foreach ($boards as $board) { - $uri = $board['uri']; - if (hasPermission($config['mod']['show_ip'], $uri)) { - $queryable_uris[] = $uri; - } - } - - $queries = $ctx->get(UserPostQueries::class); - $result = $queries->fetchPaginatedByIp($queryable_uris, $ip, $config['mod']['ip_recentposts'], $encoded_cursor); - - $args['cursor_prev'] = $result->cursor_prev; - $args['cursor_next'] = $result->cursor_next; - - foreach($boards as $board) { - $uri = $board['uri']; - // The Thread and Post classes rely on some implicit board parameter set by openBoard. - openBoard($uri); - - // Finally load the post contents and build them. - foreach ($result->by_uri[$uri] as $post) { - if (!$post['thread']) { - $po = new Thread($post, '?/', $mod, false); - } else { - $po = new Post($post, '?/', $mod); - } - - if (!isset($args['posts'][$uri])) { - $args['posts'][$uri] = [ 'board' => $board, 'posts' => [] ]; - } - $args['posts'][$uri]['posts'][] = $po->build(true); - } - } - - $args['boards'] = $boards; - $args['token'] = make_secure_link_token('ban'); - if (hasPermission($config['mod']['view_ban'])) { $args['bans'] = Bans::find($ip, false, true, $config['auto_maintenance']); } @@ -989,13 +973,52 @@ function mod_ip(Context $ctx, $ip, string $encoded_cursor = null) { $args['logs'] = []; } - if (empty($encoded_cursor)) { - $args['security_token'] = make_secure_link_token("IP/$ip"); - } else { - $args['security_token'] = make_secure_link_token("IP/$ip/cursor/$encoded_cursor"); + $boards = listBoards(); + + $queryable_uris = []; + foreach ($boards as $board) { + $uri = $board['uri']; + if (hasPermission($config['mod']['show_ip'], $uri)) { + $queryable_uris[] = $uri; + } } - mod_page(sprintf('%s: %s', _('IP'), htmlspecialchars($ip)), 'mod/view_ip.html', $args, $args['hostname']); + $queries = $ctx->get(UserPostQueries::class); + $result = $queries->fetchPaginatedByIp($queryable_uris, $ip, $page_size, $encoded_cursor); + + $args['cursor_prev'] = $result->cursor_prev; + $args['cursor_next'] = $result->cursor_next; + + foreach($boards as $board) { + $uri = $board['uri']; + // The Thread and Post classes rely on some implicit board parameter set by openBoard. + openBoard($uri); + + // Finally load the post contents and build them. + foreach ($result->by_uri[$uri] as $post) { + if (!$post['thread']) { + $po = new Thread($post, '?/', $mod, false); + } else { + $po = new Post($post, '?/', $mod); + } + + if (!isset($args['posts'][$uri])) { + $args['posts'][$uri] = [ 'board' => $board, 'posts' => [] ]; + } + $args['posts'][$uri]['posts'][] = $po->build(true); + } + } + + $args['boards'] = $boards; + $args['token'] = make_secure_link_token('ban'); + + if (empty($encoded_cursor)) { + $args['security_token'] = make_secure_link_token("user_posts/ip/$ip"); + } else { + $args['security_token'] = make_secure_link_token("user_posts/ip/$ip/cursor/$encoded_cursor"); + } + + mod_page(\sprintf('%s: %s', _('IP'), \htmlspecialchars($ip)), 'mod/view_ip.html', $args, $args['hostname']); } function mod_ban(Context $ctx) { @@ -1913,7 +1936,7 @@ function mod_ban_post(Context $ctx, $board, $delete, $post, $token = false) { $query->bindValue(':time', time()); $query->bindValue(':body', $autotag); $query->execute() or error(db_error($query)); - modLog("Added a note for {$ip}"); + modLog("Added a note for {$ip}"); } } deletePost($post); @@ -2024,7 +2047,7 @@ function mod_warning_post(Context $ctx, $board, $post, $token = false) { $query->bindValue(':time', time()); $query->bindValue(':body', $autotag); $query->execute() or error(db_error($query)); - modLog("Added a note for {$ip}"); + modLog("Added a note for {$ip}"); } } } @@ -2176,7 +2199,7 @@ function mod_delete(Context $ctx, $board, $post) { $query->bindValue(':time', time()); $query->bindValue(':body', $autotag); $query->execute() or error(db_error($query)); - modLog("Added a note for {$ip}"); + modLog("Added a note for {$ip}"); } } deletePost($post); @@ -2347,7 +2370,7 @@ function mod_deletebyip(Context $ctx, $boardName, $post, $global = false) { $query2->bindValue(':time', time()); $query2->bindValue(':body', $autotag); $query2->execute() or error(db_error($query2)); - modLog("Added a note for {$ip}"); + modLog("Added a note for {$ip}"); } } @@ -2378,7 +2401,7 @@ function mod_deletebyip(Context $ctx, $boardName, $post, $global = false) { } // Record the action - modLog("Deleted all posts by IP address: $ip"); + modLog("Deleted all posts by IP address: $ip"); // Redirect header('Location: ?/' . sprintf($config['board_path'], $boardName) . $config['file_index'], true, $config['redirect_http']); @@ -2915,7 +2938,7 @@ function mod_report_dismiss(Context $ctx, $id, $all = false) { if ($all) { $report_queries->deleteByIp($ip); - modLog("Dismissed all reports by $ip"); + modLog("Dismissed all reports by $ip"); } else { $report_queries->deleteById($id); modLog("Dismissed a report for post #{$id}", $board); From bf25002295dd04ee0701a89c8d28c57aadb25f25 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 18 Dec 2024 22:26:55 +0100 Subject: [PATCH 022/258] ip.html: use mod_user_posts_by_ip --- templates/post/ip.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/post/ip.html b/templates/post/ip.html index 11581ff9..4894394d 100644 --- a/templates/post/ip.html +++ b/templates/post/ip.html @@ -1,3 +1,3 @@ -{% if post.mod and post.mod|hasPermission(config.mod.show_ip, board.uri) %} - [{{ post.ip }}] +{% if post.mod and post.mod|hasPermission(config.mod.show_ip, board.uri) %} + [{{ post.ip }}] {# Keep this space #} {% endif %} From 720ca7787500a3eadb427425719becc1ddd4c5e2 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 18 Dec 2024 22:33:02 +0100 Subject: [PATCH 023/258] mod.php: add mod_user_posts_by_ip --- mod.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mod.php b/mod.php index 554e4fd5..d53e3cdc 100644 --- a/mod.php +++ b/mod.php @@ -71,6 +71,9 @@ $pages = [ '/IP/([\w.:]+)/cursor/([\w|-|_]+)' => 'secure_POST ip', // view ip address '/IP/([\w.:]+)/remove_note/(\d+)' => 'secure ip_remove_note', // remove note from ip address + '/user_posts/ip/([\w.:]+)' => 'secure_POST user_posts_by_ip', // view user posts by ip address + '/user_posts/ip/([\w.:]+)/cursor/([\w|-|_]+)' => 'secure_POST user_posts_by_ip', // remove note from ip address + '/ban' => 'secure_POST ban', // new ban '/bans' => 'secure_POST bans', // ban list '/bans.json' => 'secure bans_json', // ban list JSON From d29de1a9efb1c03347a05c6e7683273a71c8e7cc Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 18 Dec 2024 23:48:07 +0100 Subject: [PATCH 024/258] user_posts_list.html: add template to list posts --- templates/mod/user_posts_list.html | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 templates/mod/user_posts_list.html diff --git a/templates/mod/user_posts_list.html b/templates/mod/user_posts_list.html new file mode 100644 index 00000000..cc44bc21 --- /dev/null +++ b/templates/mod/user_posts_list.html @@ -0,0 +1,12 @@ +{% for board_posts in posts %} +
+ + + {{ config.board_abbreviation|sprintf(board_posts.board.uri) }} + - + {{ board_posts.board.title|e }} + + + {{ board_posts.posts|join('
') }} +
+{% endfor %} From 39b6a6025713954d970c70cddf6bbcfa7b130639 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 18 Dec 2024 23:48:22 +0100 Subject: [PATCH 025/258] view_ip.html: use user_posts_list.html --- templates/mod/view_ip.html | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/templates/mod/view_ip.html b/templates/mod/view_ip.html index 78fce4a9..6cc0c83f 100644 --- a/templates/mod/view_ip.html +++ b/templates/mod/view_ip.html @@ -1,4 +1,4 @@ -{% if mod|hasPermission(config.mod.view_notes) %} +{% if mod|hasPermission(config.mod.view_notes) and notes is not null %}
{% set notes_on_record = 'note' ~ (notes|count != 1 ? 's' : '') ~ ' on record' %} @@ -201,18 +201,7 @@
{% endif %} -{% for board_posts in posts %} -
- - - {{ config.board_abbreviation|sprintf(board_posts.board.uri) }} - - - {{ board_posts.board.title|e }} - - - {{ board_posts.posts|join('
') }} -
-{% endfor %} +{{ include('mod/user_posts_list.html', {posts: posts}) }}
[Page 1] {% if cursor_prev %} From 2ec23083a48e42b49e03de6eacdfcacd16257873 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 18 Dec 2024 23:50:53 +0100 Subject: [PATCH 026/258] view_ip.html: use mod_user_posts_by_ip --- templates/mod/view_ip.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/mod/view_ip.html b/templates/mod/view_ip.html index 6cc0c83f..5edbeb85 100644 --- a/templates/mod/view_ip.html +++ b/templates/mod/view_ip.html @@ -203,11 +203,11 @@ {{ include('mod/user_posts_list.html', {posts: posts}) }}
- [Page 1] + [Page 1] {% if cursor_prev %} - [Previous Page] + [Previous Page] {% endif %} {% if cursor_next %} - [Next Page] + [Next Page] {% endif %}
From 00ef803950a9e5aa019edaf70d409169a099beee Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 18 Dec 2024 23:55:40 +0100 Subject: [PATCH 027/258] ip.html: add password link --- templates/post/ip.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/post/ip.html b/templates/post/ip.html index 4894394d..84047c99 100644 --- a/templates/post/ip.html +++ b/templates/post/ip.html @@ -1,3 +1,3 @@ {% if post.mod and post.mod|hasPermission(config.mod.show_ip, board.uri) %} - [{{ post.ip }}] {# Keep this space #} + [{{ post.ip }}] [{{ post.password[:15] }}] {# Keep this space #} {% endif %} From e1514784db5aea5509e0b9520b62ebc60c88311b Mon Sep 17 00:00:00 2001 From: Zankaria Date: Thu, 19 Dec 2024 00:08:53 +0100 Subject: [PATCH 028/258] mod.php: add mod_user_posts_by_passwd --- mod.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mod.php b/mod.php index d53e3cdc..f08a465b 100644 --- a/mod.php +++ b/mod.php @@ -74,6 +74,9 @@ $pages = [ '/user_posts/ip/([\w.:]+)' => 'secure_POST user_posts_by_ip', // view user posts by ip address '/user_posts/ip/([\w.:]+)/cursor/([\w|-|_]+)' => 'secure_POST user_posts_by_ip', // remove note from ip address + '/user_posts/passwd/(\w+)' => 'secure_POST user_posts_by_passwd', // view user posts by ip address + '/user_posts/passwd/(\w+)/cursor/([\w|-|_]+)' => 'secure_POST user_posts_by_passwd', // remove note from ip address + '/ban' => 'secure_POST ban', // new ban '/bans' => 'secure_POST bans', // ban list '/bans.json' => 'secure bans_json', // ban list JSON From 0609e36ca4aafbfab875fa4dfdcbabbda60fa2da Mon Sep 17 00:00:00 2001 From: Zankaria Date: Thu, 19 Dec 2024 00:09:18 +0100 Subject: [PATCH 029/258] config.php: deprecate ip_recentposts for recent_user_posts --- inc/config.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inc/config.php b/inc/config.php index 08b01ead..095daefe 100644 --- a/inc/config.php +++ b/inc/config.php @@ -1524,8 +1524,8 @@ // Do DNS lookups on IP addresses to get their hostname for the moderator IP pages (?/IP/x.x.x.x). $config['mod']['dns_lookup'] = true; - // How many recent posts, per board, to show in each page of ?/IP/x.x.x.x. - $config['mod']['ip_recentposts'] = 5; + // How many recent posts, per board, to show in ?/user_posts/ip/x.x.x.x. and ?/user_posts/passwd/xxxxxxxx + $config['mod']['recent_user_posts'] = 5; // Number of posts to display on the reports page. $config['mod']['recent_reports'] = 10; From 10401bd094fd2456a07cff4d08f26941d0381174 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Thu, 19 Dec 2024 00:16:10 +0100 Subject: [PATCH 030/258] pages.php: add mod_user_posts_by_passwd implementation --- inc/mod/pages.php | 70 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/inc/mod/pages.php b/inc/mod/pages.php index a057ad25..0bbd7065 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -1021,6 +1021,76 @@ function mod_user_posts_by_ip(Context $ctx, string $ip, string $encoded_cursor = mod_page(\sprintf('%s: %s', _('IP'), \htmlspecialchars($ip)), 'mod/view_ip.html', $args, $args['hostname']); } +function mod_user_posts_by_passwd(Context $ctx, string $passwd, string $encoded_cursor = null) { + global $mod; + + // The current hashPassword implementation uses sha3-256, which has a 64 character output in non-binary mode. + if (\strlen($passwd) != 64) { + error('Invalid password'); + } + + $config = $ctx->get('config'); + + $args = [ + 'passwd' => $passwd, + 'posts' => [] + ]; + + if (isset($config['mod']['ip_recentposts'])) { + // TODO log to migrate. + $page_size = $config['mod']['ip_recentposts']; + } else { + $page_size = $config['mod']['recent_user_posts']; + } + + $boards = listBoards(); + + $queryable_uris = []; + foreach ($boards as $board) { + $uri = $board['uri']; + if (hasPermission($config['mod']['show_ip'], $uri)) { + $queryable_uris[] = $uri; + } + } + + $queries = $ctx->get(UserPostQueries::class); + $result = $queries->fetchPaginateByPassword($queryable_uris, $passwd, $page_size, $encoded_cursor); + + $args['cursor_prev'] = $result->cursor_prev; + $args['cursor_next'] = $result->cursor_next; + + foreach($boards as $board) { + $uri = $board['uri']; + // The Thread and Post classes rely on some implicit board parameter set by openBoard. + openBoard($uri); + + // Finally load the post contents and build them. + foreach ($result->by_uri[$uri] as $post) { + if (!$post['thread']) { + $po = new Thread($post, '?/', $mod, false); + } else { + $po = new Post($post, '?/', $mod); + } + + if (!isset($args['posts'][$uri])) { + $args['posts'][$uri] = [ 'board' => $board, 'posts' => [] ]; + } + $args['posts'][$uri]['posts'][] = $po->build(true); + } + } + + $args['boards'] = $boards; + $args['token'] = make_secure_link_token('ban'); + + if (empty($encoded_cursor)) { + $args['security_token'] = make_secure_link_token("user_posts/passwd/$passwd"); + } else { + $args['security_token'] = make_secure_link_token("user_posts/passwd/$passwd/cursor/$encoded_cursor"); + } + + mod_page(\sprintf('%s: %s', _('Password'), \htmlspecialchars($passwd)), 'mod/view_passwd.html', $args); +} + function mod_ban(Context $ctx) { global $config; From d4781d6f004dd98d14b26563a14b2a7d00816477 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Fri, 27 Dec 2024 19:13:27 +0100 Subject: [PATCH 031/258] view_passwd.html: add view password template --- templates/mod/view_passwd.html | 50 ++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 templates/mod/view_passwd.html diff --git a/templates/mod/view_passwd.html b/templates/mod/view_passwd.html new file mode 100644 index 00000000..a9fd7bdd --- /dev/null +++ b/templates/mod/view_passwd.html @@ -0,0 +1,50 @@ +{% if logs and logs|length > 0 %} +
+ History + + + + + + + + {% for log in logs %} + + + + + + + {% endfor %} +
{% trans 'Staff' %}{% trans 'Time' %}{% trans 'Board' %}{% trans 'Action' %}
+ {% if log.username %} + {{ log.username|e }} + {% elseif log.mod == -1 %} + system + {% else %} + {% trans 'deleted?' %} + {% endif %} + + {{ log.time|ago }} + + {% if log.board %} + {{ config.board_abbreviation|sprintf(log.board) }} + {% else %} + - + {% endif %} + + {{ log.text }} +
+
+{% endif %} + +{{ include('mod/user_posts_list.html', {posts: posts}) }} +
+ [Page 1] + {% if cursor_prev %} + [Previous Page] + {% endif %} + {% if cursor_next %} + [Next Page] + {% endif %} +
From 0c23445d723a88bc14cf71dee3adf099d4b096b3 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Fri, 27 Dec 2024 19:43:22 +0100 Subject: [PATCH 032/258] pages.php: slightly better ip error --- inc/mod/pages.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/inc/mod/pages.php b/inc/mod/pages.php index 0bbd7065..f743528f 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -854,7 +854,7 @@ function mod_ip_remove_note(Context $ctx, $ip, $id) { error($config['error']['noaccess']); if (filter_var($ip, FILTER_VALIDATE_IP) === false) - error("Invalid IP address."); + error('Invalid IP address'); $query = prepare('DELETE FROM ``ip_notes`` WHERE `ip` = :ip AND `id` = :id'); $query->bindValue(':ip', $ip); @@ -870,7 +870,7 @@ function mod_ip(Context $ctx, $ip, string $encoded_cursor = null) { global $config, $mod; if (filter_var($ip, FILTER_VALIDATE_IP) === false) - error("Invalid IP address."); + error('Invalid IP address'); if (isset($_POST['ban_id'], $_POST['unban'])) { if (!hasPermission($config['mod']['unban'])) @@ -922,7 +922,7 @@ function mod_user_posts_by_ip(Context $ctx, string $ip, string $encoded_cursor = global $mod; if (\filter_var($ip, \FILTER_VALIDATE_IP) === false){ - error("Invalid IP address."); + error('Invalid IP address'); } $config = $ctx->get('config'); From a45106b65f664c9e997a8de901ce39d50cc6d8e5 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Fri, 27 Dec 2024 19:59:02 +0100 Subject: [PATCH 033/258] pages.php: adjust redirect on mod_ip --- inc/mod/pages.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/inc/mod/pages.php b/inc/mod/pages.php index f743528f..3e0967f1 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -911,10 +911,11 @@ function mod_ip(Context $ctx, $ip, string $encoded_cursor = null) { return; } + // Temporary Redirect so to not to break the note and unban system. if (empty($encoded_cursor)) { - \header("Location: ?/user_posts/ip/$ip", true, 308); + \header("Location: ?/user_posts/ip/$ip", true, 307); } else { - \header("Location: ?/user_posts/ip/$ip/cursor/$encoded_cursor", true, 308); + \header("Location: ?/user_posts/ip/$ip/cursor/$encoded_cursor", true, 307); } } From 4b49019282b33e71dcbfe70e31ff6b1b8d57773f Mon Sep 17 00:00:00 2001 From: Zankaria Date: Fri, 27 Dec 2024 20:14:47 +0100 Subject: [PATCH 034/258] pages.php: QUICKFIX handle unban and notes in mod_user_posts_by_ip to workaround security token issue --- inc/mod/pages.php | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/inc/mod/pages.php b/inc/mod/pages.php index 3e0967f1..56785056 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -928,6 +928,45 @@ function mod_user_posts_by_ip(Context $ctx, string $ip, string $encoded_cursor = $config = $ctx->get('config'); + if (isset($_POST['ban_id'], $_POST['unban'])) { + if (!hasPermission($config['mod']['unban'])) + error($config['error']['noaccess']); + + Bans::delete($_POST['ban_id'], true, $mod['boards']); + + if (empty($encoded_cursor)) { + \header("Location: ?/user_posts/ip/$ip#bans", true, $config['redirect_http']); + } else { + \header("Location: ?/user_posts/ip/$ip/cursor/$encoded_cursor#bans", true, $config['redirect_http']); + } + return; + } + + if (isset($_POST['note'])) { + if (!hasPermission($config['mod']['create_notes'])) + error($config['error']['noaccess']); + + $_POST['note'] = escape_markup_modifiers($_POST['note']); + markup($_POST['note']); + $query = prepare('INSERT INTO ``ip_notes`` VALUES (NULL, :ip, :mod, :time, :body)'); + $query->bindValue(':ip', $ip); + $query->bindValue(':mod', $mod['id']); + $query->bindValue(':time', time()); + $query->bindValue(':body', $_POST['note']); + $query->execute() or error(db_error($query)); + + Cache::delete("mod_page_ip_view_notes_$ip"); + + modLog("Added a note for {$ip}"); + + if (empty($encoded_cursor)) { + \header("Location: ?/user_posts/ip/$ip#notes", true, $config['redirect_http']); + } else { + \header("Location: ?/user_posts/ip/$ip/cursor/$encoded_cursor#notes", true, $config['redirect_http']); + } + return; + } + $args = [ 'ip' => $ip, 'posts' => [] From e87f50407c748a2d25ca749b2f2a50c631362868 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Sun, 29 Dec 2024 00:01:09 +0100 Subject: [PATCH 035/258] pages.php: change security token of mod_user_posts_by_ip --- inc/mod/pages.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/inc/mod/pages.php b/inc/mod/pages.php index 56785056..2cb2c22f 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -1052,10 +1052,11 @@ function mod_user_posts_by_ip(Context $ctx, string $ip, string $encoded_cursor = $args['boards'] = $boards; $args['token'] = make_secure_link_token('ban'); + // Since the security token is only used to send requests to create notes and remove bans, use "?/IP/" as the url. if (empty($encoded_cursor)) { - $args['security_token'] = make_secure_link_token("user_posts/ip/$ip"); + $args['security_token'] = make_secure_link_token("IP/$ip"); } else { - $args['security_token'] = make_secure_link_token("user_posts/ip/$ip/cursor/$encoded_cursor"); + $args['security_token'] = make_secure_link_token("IP/$ip/cursor/$encoded_cursor"); } mod_page(\sprintf('%s: %s', _('IP'), \htmlspecialchars($ip)), 'mod/view_ip.html', $args, $args['hostname']); From c718eb70b0fefe92da5795ffb6a9d20e1a4d100d Mon Sep 17 00:00:00 2001 From: Zankaria Date: Sun, 29 Dec 2024 00:01:41 +0100 Subject: [PATCH 036/258] Revert "pages.php: QUICKFIX handle unban and notes in mod_user_posts_by_ip to workaround security token issue" This reverts commit 4b49019282b33e71dcbfe70e31ff6b1b8d57773f. --- inc/mod/pages.php | 39 --------------------------------------- 1 file changed, 39 deletions(-) diff --git a/inc/mod/pages.php b/inc/mod/pages.php index 2cb2c22f..6a11e483 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -928,45 +928,6 @@ function mod_user_posts_by_ip(Context $ctx, string $ip, string $encoded_cursor = $config = $ctx->get('config'); - if (isset($_POST['ban_id'], $_POST['unban'])) { - if (!hasPermission($config['mod']['unban'])) - error($config['error']['noaccess']); - - Bans::delete($_POST['ban_id'], true, $mod['boards']); - - if (empty($encoded_cursor)) { - \header("Location: ?/user_posts/ip/$ip#bans", true, $config['redirect_http']); - } else { - \header("Location: ?/user_posts/ip/$ip/cursor/$encoded_cursor#bans", true, $config['redirect_http']); - } - return; - } - - if (isset($_POST['note'])) { - if (!hasPermission($config['mod']['create_notes'])) - error($config['error']['noaccess']); - - $_POST['note'] = escape_markup_modifiers($_POST['note']); - markup($_POST['note']); - $query = prepare('INSERT INTO ``ip_notes`` VALUES (NULL, :ip, :mod, :time, :body)'); - $query->bindValue(':ip', $ip); - $query->bindValue(':mod', $mod['id']); - $query->bindValue(':time', time()); - $query->bindValue(':body', $_POST['note']); - $query->execute() or error(db_error($query)); - - Cache::delete("mod_page_ip_view_notes_$ip"); - - modLog("Added a note for {$ip}"); - - if (empty($encoded_cursor)) { - \header("Location: ?/user_posts/ip/$ip#notes", true, $config['redirect_http']); - } else { - \header("Location: ?/user_posts/ip/$ip/cursor/$encoded_cursor#notes", true, $config['redirect_http']); - } - return; - } - $args = [ 'ip' => $ip, 'posts' => [] From e52465753e6994eb045d83206942fcfcf73dd54f Mon Sep 17 00:00:00 2001 From: Zankaria Date: Sun, 29 Dec 2024 00:19:17 +0100 Subject: [PATCH 037/258] pages.php: remove seemingly unused code --- inc/mod/pages.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/inc/mod/pages.php b/inc/mod/pages.php index 6a11e483..bf40d7a9 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -1011,7 +1011,6 @@ function mod_user_posts_by_ip(Context $ctx, string $ip, string $encoded_cursor = } $args['boards'] = $boards; - $args['token'] = make_secure_link_token('ban'); // Since the security token is only used to send requests to create notes and remove bans, use "?/IP/" as the url. if (empty($encoded_cursor)) { @@ -1082,7 +1081,6 @@ function mod_user_posts_by_passwd(Context $ctx, string $passwd, string $encoded_ } $args['boards'] = $boards; - $args['token'] = make_secure_link_token('ban'); if (empty($encoded_cursor)) { $args['security_token'] = make_secure_link_token("user_posts/passwd/$passwd"); From 7c982da304387ad8622af70cc0f839d848644cf3 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Sun, 29 Dec 2024 00:19:48 +0100 Subject: [PATCH 038/258] view_ip.html: use ?/IP endpoint to remove bans and add notes --- templates/mod/view_ip.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/mod/view_ip.html b/templates/mod/view_ip.html index 5edbeb85..8060c28a 100644 --- a/templates/mod/view_ip.html +++ b/templates/mod/view_ip.html @@ -43,7 +43,7 @@ {% endif %} {% if mod|hasPermission(config.mod.create_notes) %} -
+ @@ -74,7 +74,7 @@ {{ bans|count }} {% trans bans_on_record %} {% for ban in bans %} - +
From 34c521aab1a8b3558c1ba0817dd36f3e89946670 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Sun, 29 Dec 2024 00:19:17 +0100 Subject: [PATCH 039/258] pages.php: remove seemingly unused code --- inc/mod/pages.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/inc/mod/pages.php b/inc/mod/pages.php index 56785056..77540e7e 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -1050,7 +1050,6 @@ function mod_user_posts_by_ip(Context $ctx, string $ip, string $encoded_cursor = } $args['boards'] = $boards; - $args['token'] = make_secure_link_token('ban'); if (empty($encoded_cursor)) { $args['security_token'] = make_secure_link_token("user_posts/ip/$ip"); @@ -1120,7 +1119,6 @@ function mod_user_posts_by_passwd(Context $ctx, string $passwd, string $encoded_ } $args['boards'] = $boards; - $args['token'] = make_secure_link_token('ban'); if (empty($encoded_cursor)) { $args['security_token'] = make_secure_link_token("user_posts/passwd/$passwd"); From 87a30b8aabcdd654027464d2d2cfb5531e1beee6 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Sun, 29 Dec 2024 00:53:05 +0100 Subject: [PATCH 040/258] show-backlinks.js: add support for op post --- js/show-backlinks.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/js/show-backlinks.js b/js/show-backlinks.js index 5924124e..3a20ae01 100644 --- a/js/show-backlinks.js +++ b/js/show-backlinks.js @@ -26,12 +26,17 @@ $(document).ready(function() { } let post = $('#reply_' + id); - if(post.length == 0) - return; + if (post.length == 0) { + post = $('#op_' + id); + if (post.length == 0) { + return; + } + } let mentioned = post.find('.head div.mentioned'); if (mentioned.length === 0) { - mentioned = $('
').prependTo(post.find('.head')); + // The op has two "head"s divs, use the second. + mentioned = $('
').prependTo(post.find('.head').last()); } if (mentioned.find('a.mentioned-' + replyId).length !== 0) { @@ -48,13 +53,13 @@ $(document).ready(function() { }); }; - $('div.post.reply').each(showBackLinks); + $('div.post').each(showBackLinks); $(document).on('new_post', function(e, post) { - if ($(post).hasClass('reply')) { + if ($(post).hasClass('reply') || $(post).hasClass('op')) { showBackLinks.call(post); } else { - $(post).find('div.post.reply').each(showBackLinks); + $(post).find('div.post').each(showBackLinks); } }); }); From cd2a4b5fac4a26e177814afb313b570197ec15c1 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Sun, 29 Dec 2024 15:13:07 +0100 Subject: [PATCH 041/258] Revert "pages.php: remove seemingly unused code" This reverts commit e52465753e6994eb045d83206942fcfcf73dd54f. --- inc/mod/pages.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/inc/mod/pages.php b/inc/mod/pages.php index bf40d7a9..6a11e483 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -1011,6 +1011,7 @@ function mod_user_posts_by_ip(Context $ctx, string $ip, string $encoded_cursor = } $args['boards'] = $boards; + $args['token'] = make_secure_link_token('ban'); // Since the security token is only used to send requests to create notes and remove bans, use "?/IP/" as the url. if (empty($encoded_cursor)) { @@ -1081,6 +1082,7 @@ function mod_user_posts_by_passwd(Context $ctx, string $passwd, string $encoded_ } $args['boards'] = $boards; + $args['token'] = make_secure_link_token('ban'); if (empty($encoded_cursor)) { $args['security_token'] = make_secure_link_token("user_posts/passwd/$passwd"); From 66b1e7d5690a4aaa01a85495a8536cae37876426 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Sun, 29 Dec 2024 15:33:10 +0100 Subject: [PATCH 042/258] show-backlinks.js: simplify --- js/show-backlinks.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/js/show-backlinks.js b/js/show-backlinks.js index 3a20ae01..f0671e80 100644 --- a/js/show-backlinks.js +++ b/js/show-backlinks.js @@ -25,12 +25,9 @@ $(document).ready(function() { return; } - let post = $('#reply_' + id); + let post = $('#reply_' + id + ', #op_' + id); if (post.length == 0) { - post = $('#op_' + id); - if (post.length == 0) { - return; - } + return; } let mentioned = post.find('.head div.mentioned'); From 7e881e2ec3b9b2c3c2d22f0386db233cb5aeacd8 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Tue, 7 Jan 2025 18:29:03 +0100 Subject: [PATCH 043/258] style.css: remove extra padding from OP --- stylesheets/style.css | 6 ------ 1 file changed, 6 deletions(-) diff --git a/stylesheets/style.css b/stylesheets/style.css index db434302..9d6ef686 100644 --- a/stylesheets/style.css +++ b/stylesheets/style.css @@ -592,12 +592,6 @@ div.post div.body { div.post.op { padding-top: 0px; vertical-align: top; - - /* Add back in the padding that is provided by body on large screens */ - @media (max-width: 48em) { - padding-left: 4px; - padding-right: 4px; - } } div.post.reply { From 020d66ffdc5bb330f8b45f7638940507994355dd Mon Sep 17 00:00:00 2001 From: Zankaria Date: Tue, 7 Jan 2025 18:39:31 +0100 Subject: [PATCH 044/258] style.css: harmonize post body margins --- stylesheets/style.css | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/stylesheets/style.css b/stylesheets/style.css index 9d6ef686..e4b9f844 100644 --- a/stylesheets/style.css +++ b/stylesheets/style.css @@ -565,16 +565,11 @@ div.post.op > p { div.post div.body { margin-top: 0.8em; + margin-left: 1.4em; padding-right: 3em; padding-bottom: 0.3em; -} -div.post.op div.body { - margin-left: 0.8em; -} - -div.post.reply div.body { - margin-left: 1.8em; + white-space: pre-wrap; } div.post.reply.highlighted { @@ -585,10 +580,6 @@ div.post.reply div.body a { color: #D00; } -div.post div.body { - white-space: pre-wrap; -} - div.post.op { padding-top: 0px; vertical-align: top; From 22626b4a6fe4cb6d9834d87b4d9089e126f8ccda Mon Sep 17 00:00:00 2001 From: Zankaria Date: Tue, 7 Jan 2025 18:48:12 +0100 Subject: [PATCH 045/258] style.css: harmonize head margin --- stylesheets/style.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stylesheets/style.css b/stylesheets/style.css index e4b9f844..12e5a4c5 100644 --- a/stylesheets/style.css +++ b/stylesheets/style.css @@ -538,8 +538,8 @@ div.post { } } -div.post > div.head { - margin: 0.1em 1em; +div.post div.head { + margin: 0.1em 1em 0.1em 1.4em; clear: both; line-height: 1.3em; } From 55d21a4dd546bb57b59270bcce43238e4f8a0264 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Tue, 7 Jan 2025 18:55:08 +0100 Subject: [PATCH 046/258] style.css: set the margin of omitted post indicator like margin on intro --- stylesheets/style.css | 1 + 1 file changed, 1 insertion(+) diff --git a/stylesheets/style.css b/stylesheets/style.css index 12e5a4c5..78bc7635 100644 --- a/stylesheets/style.css +++ b/stylesheets/style.css @@ -633,6 +633,7 @@ span.trip { span.omitted { display: block; margin-top: 1em; + margin-left: 0.4em; } br.clear { From 7de4ca2133de997e8106c487f2eebcc5182282a0 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 8 Jan 2025 22:14:21 +0100 Subject: [PATCH 047/258] css: add missing media queries to css styles --- stylesheets/500px.css | 5 + stylesheets/8ch.cyber.css | 5 + stylesheets/beta.css | 5 + stylesheets/bunker_like.css | 5 + stylesheets/cyberpunk.css | 15 +- stylesheets/cyberpunk2.css | 15 +- stylesheets/dark.css | 5 + stylesheets/dark_red.css | 5 + stylesheets/dark_spook.css | 5 + stylesheets/darkroot_garden.css | 17 +- stylesheets/delete.css | Bin 12284 -> 12444 bytes stylesheets/fauux.css | 13 +- stylesheets/ferus.css | 9 +- stylesheets/forest.css | 17 +- stylesheets/futaba-light.css | 8 +- stylesheets/gentoochan.css | 9 +- stylesheets/gorby.css | 8 + stylesheets/heavy_ice.css | 5 + stylesheets/jungle.css | 8 + stylesheets/kalyx_theme.css | 99 +-- stylesheets/midnight.css | 18 +- stylesheets/mono.e.lain.css | 5 + stylesheets/muted.css | 18 +- stylesheets/northboard_cb.css | 22 +- stylesheets/terminal2.css | 5 + stylesheets/terminal_common.css | 5 + stylesheets/test.css | 5 + stylesheets/tsuki.css | 1149 ++++++++++++++++--------------- 28 files changed, 819 insertions(+), 666 deletions(-) diff --git a/stylesheets/500px.css b/stylesheets/500px.css index 89b1fa64..5f733005 100644 --- a/stylesheets/500px.css +++ b/stylesheets/500px.css @@ -152,6 +152,11 @@ div.post.reply.highlighted box-shadow: 3px 5px #5c8c8e; margin-left: auto; margin-right: auto; + + @media (max-width: 48em) { + border-left-style: none; + border-right-style: none; + } } div.post.reply div.body a:link, div.post.reply div.body a:visited { diff --git a/stylesheets/8ch.cyber.css b/stylesheets/8ch.cyber.css index c94c50e8..330eadd3 100644 --- a/stylesheets/8ch.cyber.css +++ b/stylesheets/8ch.cyber.css @@ -219,6 +219,11 @@ background-color: #2b2b2b; background-color: #2b2b2b; border: solid 1px #93e0e3; box-shadow: 3px 5px #93e0e3; + + @media (max-width: 48em) { + border-left-style: none; + border-right-style: none; + } } /*dont touch this*/ diff --git a/stylesheets/beta.css b/stylesheets/beta.css index e32353cc..6a2c771e 100644 --- a/stylesheets/beta.css +++ b/stylesheets/beta.css @@ -161,6 +161,11 @@ div.post.reply.highlighted box-shadow: 3px 5px #5c8c8e; margin-left: auto; margin-right: auto; + + @media (max-width: 48em) { + border-left-style: none; + border-right-style: none; + } } div.post.reply div.body a:link, div.post.reply div.body a:visited { diff --git a/stylesheets/bunker_like.css b/stylesheets/bunker_like.css index a69591a1..507003ce 100644 --- a/stylesheets/bunker_like.css +++ b/stylesheets/bunker_like.css @@ -120,6 +120,11 @@ div.post.reply.highlighted { background: rgba(59, 22, 43, 0.4); border: 1px solid #117743; border-radius: 5px; + + @media (max-width: 48em) { + border-left-style: none; + border-right-style: none; + } } /* POST CONTENT */ diff --git a/stylesheets/cyberpunk.css b/stylesheets/cyberpunk.css index 35fdbbb7..15b68ed5 100644 --- a/stylesheets/cyberpunk.css +++ b/stylesheets/cyberpunk.css @@ -1,7 +1,7 @@ /*cyberpunk mod of ferus by kalyx B332E6 = dark purp -33cccc = teal +33cccc = teal 00ff00 = green FF46D2 = pink dark blue = 00080C @@ -37,7 +37,7 @@ div.boardlist{ background-color: #0C0001; } -@font-face +@font-face { font-family: 'lain'; src: url('./fonts/nrdyyh.woff') format('woff'), @@ -84,7 +84,7 @@ a:link, a:visited, p.intro a.email span.name -ms-transition: 0.15s text-shadow, 0.15s color; transition: 0.15s text-shadow, 0.15s color; } -input[type="text"], textarea +input[type="text"], textarea { -moz-transition: 0.15s border-color; -webkit-transition: 0.15s border-color; @@ -93,7 +93,7 @@ input[type="text"], textarea -ms-transition: 0.15s border-color; transition: 0.15s border-color; } -input[type="submit"] +input[type="submit"] { -moz-transition: 0.15s border-color, 0.15s background-color, 0.15s color; -webkit-transition: 0.15s border-color, 0.15s background-color, 0.15s color; @@ -117,7 +117,7 @@ a.post_no { color: #33cccc; text-decoration: none; } -span.quote +span.quote { color:#00ff00; } @@ -136,6 +136,11 @@ div.post.reply { div.post.reply.highlighted { background: transparent; border: #1A6666 2px solid; + + @media (max-width: 48em) { + border-left-style: none; + border-right-style: none; + } } div.post.reply div.body a:link, div.post.reply div.body a:visited { color: #646464; diff --git a/stylesheets/cyberpunk2.css b/stylesheets/cyberpunk2.css index 2e907966..4754a882 100644 --- a/stylesheets/cyberpunk2.css +++ b/stylesheets/cyberpunk2.css @@ -1,7 +1,7 @@ /*cyberpunk mod of ferus by kalyx B332E6 = dark purp -33cccc = teal +33cccc = teal 00ff00 = green FF46D2 = pink dark blue = 00080C @@ -14,7 +14,7 @@ body { font-size: 11px; /*text-shadow: 0px 0px 3px #FF46D2;*/ } -@font-face +@font-face { font-family: 'lain'; src: url('./fonts/nrdyyh.woff') format('woff'), @@ -61,7 +61,7 @@ a:link, a:visited, p.intro a.email span.name -ms-transition: 0.15s text-shadow, 0.15s color; transition: 0.15s text-shadow, 0.15s color; } -input[type="text"], textarea +input[type="text"], textarea { -moz-transition: 0.15s border-color; -webkit-transition: 0.15s border-color; @@ -70,7 +70,7 @@ input[type="text"], textarea -ms-transition: 0.15s border-color; transition: 0.15s border-color; } -input[type="submit"] +input[type="submit"] { -moz-transition: 0.15s border-color, 0.15s background-color, 0.15s color; -webkit-transition: 0.15s border-color, 0.15s background-color, 0.15s color; @@ -94,7 +94,7 @@ a.post_no { color: #33cccc; text-decoration: none; } -span.quote +span.quote { color:#00ff00; } @@ -113,6 +113,11 @@ div.post.reply { div.post.reply.highlighted { background: transparent; border: #33cccc 2px solid; + + @media (max-width: 48em) { + border-left-style: none; + border-right-style: none; + } } div.post.reply div.body a:link, div.post.reply div.body a:visited { color: #646464; diff --git a/stylesheets/dark.css b/stylesheets/dark.css index dd7e9bfd..9f3df691 100644 --- a/stylesheets/dark.css +++ b/stylesheets/dark.css @@ -63,6 +63,11 @@ div.post.reply { div.post.reply.highlighted { background: #555; border: transparent 1px solid; + + @media (max-width: 48em) { + border-left-style: none; + border-right-style: none; + } } div.post.reply div.body a:link, div.post.reply div.body a:visited { color: #CCCCCC; diff --git a/stylesheets/dark_red.css b/stylesheets/dark_red.css index 8593ead9..eec8a1f4 100644 --- a/stylesheets/dark_red.css +++ b/stylesheets/dark_red.css @@ -60,6 +60,11 @@ div.post.reply { div.post.reply.highlighted { background: #555; border: transparent 1px solid; + + @media (max-width: 48em) { + border-left-style: none; + border-right-style: none; + } } div.post.reply div.body a:link, div.post.reply div.body a:visited { color: #CCCCCC; diff --git a/stylesheets/dark_spook.css b/stylesheets/dark_spook.css index 5e9bf837..2ca55a20 100644 --- a/stylesheets/dark_spook.css +++ b/stylesheets/dark_spook.css @@ -61,6 +61,11 @@ div.post.reply { div.post.reply.highlighted { background: #555; border: transparent 1px solid; + + @media (max-width: 48em) { + border-left-style: none; + border-right-style: none; + } } div.post.reply div.body a:link, div.post.reply div.body a:visited { diff --git a/stylesheets/darkroot_garden.css b/stylesheets/darkroot_garden.css index 22ee517d..cd27da0a 100644 --- a/stylesheets/darkroot_garden.css +++ b/stylesheets/darkroot_garden.css @@ -1,7 +1,7 @@ /*cyberpunk mod of ferus by kalyx B332E6 = dark purp -293728 = teal +293728 = teal 59B451 = green FF46D2 = pink dark blue = 293728 @@ -19,13 +19,13 @@ div.boardlist{ } -@font-face +@font-face { font-family: 'lain'; src: url('./fonts/nrdyyh.woff') format('woff'), url('./fonts/tojcxo.TTF') format('truetype'); } - @font-face + @font-face { font-family: 'DejaVuSansMono'; src: url('./fonts/DejaVuSansMono.ttf') format('truetype'); @@ -79,7 +79,7 @@ a:link, a:visited, p.intro a.email span.name -ms-transition: 0.15s text-shadow, 0.15s color; transition: 0.15s text-shadow, 0.15s color; } -input[type="text"], textarea +input[type="text"], textarea { -moz-transition: 0.15s border-color; -webkit-transition: 0.15s border-color; @@ -88,7 +88,7 @@ input[type="text"], textarea -ms-transition: 0.15s border-color; transition: 0.15s border-color; } -input[type="submit"] +input[type="submit"] { -moz-transition: 0.15s border-color, 0.15s background-color, 0.15s color; -webkit-transition: 0.15s border-color, 0.15s background-color, 0.15s color; @@ -112,7 +112,7 @@ a.post_no { color: #293728; text-decoration: none; } -span.quote +span.quote { color:#4CADA7; } @@ -131,6 +131,11 @@ div.post.reply { div.post.reply.highlighted { background: transparent; border: #293728 1px solid; + + @media (max-width: 48em) { + border-left-style: none; + border-right-style: none; + } } div.post.reply div.body a:link, div.post.reply div.body a:visited { color: #646464; diff --git a/stylesheets/delete.css b/stylesheets/delete.css index 32bc764de6fe6719fbb8a9164ea72ed97f78bb59..7d7a687655c229f8bff680e30156352066fa77bb 100644 GIT binary patch delta 223 zcmewpKPPcR1mEN}d`gq|NGWVy#P^78@;+uZeg}qJhE#?WhD?S;1_cI<$?JthEfp9{ z7%YIIxeS^>v1$fh1}+9pAWUM&XD9-yO$Cy=3^_nLjiCg{Dh863ApYcy0>bK;DvN-o zr!!<=s%7S6sGYn~U3BsVL7~YBVhWp&iEiSZtiY+Wd6}{h)8tQ5Qj-O=gf>4@J;XTK fNk(Y$1UZh)N7Q}TfZpJmte__XqBlF~?P3G~?p`^@ delta 93 zcmbP}_$Pit1m9#GeU8Zr%6gM4q(wH*;(Nq4S&NBp@*gpi&1GVnco`KY%X8{(4p9+e vV&t5>P+EF(gW45FAoHV)@MH&hj?H-*K5VQC3_v(}r=C1BkluV Date: Mon, 20 Jan 2025 19:59:49 +0100 Subject: [PATCH 048/258] faq: update faq matrix antichamber link --- templates/themes/faq/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/themes/faq/index.html b/templates/themes/faq/index.html index ffa47872..c5941e5a 100644 --- a/templates/themes/faq/index.html +++ b/templates/themes/faq/index.html @@ -26,7 +26,7 @@

My question isn't here. What do I do ?

-

Make a post in the /meta/ board or ask via our Matrix Congress chat

+

Make a post in the /meta/ board or ask via our Matrix Congress chat

What are the rules ?

From 0af5185dd92cf0cb2d001ad23189e115df76466f Mon Sep 17 00:00:00 2001 From: Zankaria Date: Tue, 21 Jan 2025 21:58:52 +0100 Subject: [PATCH 049/258] pages.php: use context to fetch config in mod_view_catalog --- inc/mod/pages.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/inc/mod/pages.php b/inc/mod/pages.php index 6a11e483..6b4a85d0 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -765,7 +765,8 @@ function mod_board_log(Context $ctx, $board, $page_no = 1, $hide_names = false, } function mod_view_catalog(Context $ctx, $boardName) { - global $config; + $config = $ctx->get('config'); + require_once($config['dir']['themes'].'/catalog/theme.php'); $settings = []; $settings['boards'] = $boardName; From d30e0a1a9b00065ba22a87dcb947d64a38dfc161 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Tue, 21 Jan 2025 21:59:25 +0100 Subject: [PATCH 050/258] pages.php: add missing mod global --- inc/mod/pages.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/inc/mod/pages.php b/inc/mod/pages.php index 6b4a85d0..380a5627 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -765,6 +765,8 @@ function mod_board_log(Context $ctx, $board, $page_no = 1, $hide_names = false, } function mod_view_catalog(Context $ctx, $boardName) { + global $mod; + $config = $ctx->get('config'); require_once($config['dir']['themes'].'/catalog/theme.php'); From 216477177f169279ac9b62b950ae7542dcd03b8d Mon Sep 17 00:00:00 2001 From: Zankaria Date: Tue, 21 Jan 2025 22:17:14 +0100 Subject: [PATCH 051/258] faq: Text file thumbnail generation is not supported --- templates/themes/faq/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/themes/faq/index.html b/templates/themes/faq/index.html index c5941e5a..4f03b0d8 100644 --- a/templates/themes/faq/index.html +++ b/templates/themes/faq/index.html @@ -121,7 +121,7 @@
  • PDF Files (Supports thumbnail)
  • EPUB Files
  • DJVU Files (Supports thumbnail)
  • -
  • Text Files (Supports thumbnail)
  • +
  • Text Files
  • ZIP Files
  • GZ Files
  • BZ2 Files
  • From ceb6638b998ab16fa59ea1ad048843454b5de8eb Mon Sep 17 00:00:00 2001 From: Zankaria Date: Tue, 21 Jan 2025 22:19:01 +0100 Subject: [PATCH 052/258] faq: fix wrong matrix link --- templates/themes/faq/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/themes/faq/index.html b/templates/themes/faq/index.html index 4f03b0d8..11637f07 100644 --- a/templates/themes/faq/index.html +++ b/templates/themes/faq/index.html @@ -26,7 +26,7 @@

    My question isn't here. What do I do ?

    -

    Make a post in the /meta/ board or ask via our Matrix Congress chat

    +

    Make a post in the /meta/ board or ask via our Matrix Congress chat

    What are the rules ?

    From 7a50a603ae7ed262921aa4ced438f3caa875ee37 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Tue, 21 Jan 2025 22:51:29 +0100 Subject: [PATCH 053/258] news.html: add missing meta tags --- templates/themes/categories/news.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/templates/themes/categories/news.html b/templates/themes/categories/news.html index 4af4e21c..484c4eb0 100644 --- a/templates/themes/categories/news.html +++ b/templates/themes/categories/news.html @@ -16,7 +16,10 @@ {{ boardlist.top }}
    -

    {{ settings.title }}

    + + + {% if config.meta_keywords %}{% endif %} + {{ settings.title }}
    {{ settings.subtitle }}
    From 7cea9fb942772eafe0f31415575dfd7486a9a811 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Tue, 21 Jan 2025 22:51:50 +0100 Subject: [PATCH 054/258] faq: remove duplicated header tag --- templates/themes/faq/index.html | 1 - 1 file changed, 1 deletion(-) diff --git a/templates/themes/faq/index.html b/templates/themes/faq/index.html index 11637f07..6ddf872a 100644 --- a/templates/themes/faq/index.html +++ b/templates/themes/faq/index.html @@ -4,7 +4,6 @@ - {{ settings.title }} {% if config.meta_keywords %}{% endif %} {% endif %} {% endif %} - {% if config.recaptcha %} + {% if config.captcha.mode == 'recaptcha' %} {% endif %} From d34b1e105e8d799e3e885140757cc8b66b0a5a2c Mon Sep 17 00:00:00 2001 From: Zankaria Date: Sat, 8 Feb 2025 18:14:32 +0100 Subject: [PATCH 076/258] captcha_script.html: use new config style --- templates/captcha_script.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/captcha_script.html b/templates/captcha_script.html index 79b0b230..c2f5d87f 100644 --- a/templates/captcha_script.html +++ b/templates/captcha_script.html @@ -1,6 +1,6 @@ -{% if config.hcaptcha %} +{% if config.captcha.mode == 'hcaptcha' %} {% endif %} -{% if config.turnstile %} +{% if config.captcha.mode == 'turnstile' %} {% endif %} From 4eb12479ec2d76f7369f9927097634dbe0c14c15 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Sat, 8 Feb 2025 18:14:52 +0100 Subject: [PATCH 077/258] post_form.html: use new config style --- templates/post_form.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/templates/post_form.html b/templates/post_form.html index adca9ef6..6cb0ea7a 100644 --- a/templates/post_form.html +++ b/templates/post_form.html @@ -90,8 +90,8 @@ {% endif %}
    {% endif %} - {% if config.recaptcha %} - {% if config.dynamic_captcha %} + {% if config.captcha.mode == 'recaptcha' %} + {% if config.captcha.dynamic %} {% else %} @@ -101,13 +101,13 @@ {{ antibot.html() }} {% endif %} - {% if config.hcaptcha or config.turnstile %} - {% if config.dynamic_captcha %} + {% if config.captcha.mode == 'hcaptcha' or config.captcha.mode == 'turnstile' %} + {% if config.captcha.dynamic %} {% else %} From a70b6e6ec98e277c238725ba4a70614118304ee3 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Sat, 8 Feb 2025 18:15:07 +0100 Subject: [PATCH 078/258] main.js: use new config style --- templates/main.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/templates/main.js b/templates/main.js index d7ae7fae..574a3611 100755 --- a/templates/main.js +++ b/templates/main.js @@ -243,13 +243,13 @@ function getCookie(cookie_name) { {% endraw %} /* BEGIN CAPTCHA REGION */ -{% if config.hcaptcha or config.turnstile %} // If any captcha +{% if config.captcha.mode == 'hcaptcha' or config.captcha.mode == 'turnstile' %} // If any captcha // Global captcha object. Assigned by `onCaptchaLoad()`. var captcha_renderer = null; // Captcha widget id of the post form. var postCaptchaId = null; -{% if config.hcaptcha %} // If hcaptcha +{% if config.captcha.mode == 'hcaptcha' %} // If hcaptcha function onCaptchaLoadHcaptcha() { if (captcha_renderer === null && (active_page === 'index' || active_page === 'catalog' || active_page === 'thread')) { let renderer = { @@ -257,7 +257,7 @@ function onCaptchaLoadHcaptcha() { * @returns {object} Opaque widget id. */ applyOn: (container, params) => hcaptcha.render(container, { - sitekey: "{{ config.hcaptcha_public }}", + sitekey: "{{ config.captcha.hcaptcha.public }}", callback: params['on-success'], }), /** @@ -282,7 +282,7 @@ function onCaptchaLoadHcaptcha() { } } {% endif %} // End if hcaptcha -{% if config.turnstile %} // If turnstile +{% if config.captcha.mode == 'turnstile' %} // If turnstile // Wrapper function to be called from thread.html window.onCaptchaLoadTurnstile_post_reply = function() { @@ -303,7 +303,7 @@ function onCaptchaLoadTurnstile(action) { */ applyOn: function(container, params) { let widgetId = turnstile.render('#' + container, { - sitekey: "{{ config.turnstile_public }}", + sitekey: "{{ config.captcha.turnstile.public }}", action: action, callback: params['on-success'], }); From 222c4c4d275f055df7e0fac8f7d50720562f5179 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Sat, 8 Feb 2025 18:47:07 +0100 Subject: [PATCH 079/258] ajax.js: do not send request if captcha does not have a response --- js/ajax.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/js/ajax.js b/js/ajax.js index af795a57..d2319f39 100644 --- a/js/ajax.js +++ b/js/ajax.js @@ -18,7 +18,13 @@ $(window).ready(function() { // Enable submit button if disabled (cache problem) $('input[type="submit"]').removeAttr('disabled'); - + + // In the captcha is present, halt if it does not have a response.. + if (captcha_renderer && postCaptchaId && !captcha_renderer.hasResponse(postCaptchaId)) { + captcha_renderer.execute(postCaptchaId); + return false; + } + var setup_form = function($form) { $form.submit(function() { if (do_not_ajax) @@ -27,7 +33,7 @@ $(window).ready(function() { var submit_txt = $(this).find('input[type="submit"]').val(); if (window.FormData === undefined) return true; - + var formData = new FormData(this); formData.append('json_response', '1'); formData.append('post', submit_txt); @@ -94,11 +100,11 @@ $(window).ready(function() { setTimeout(function() { $(window).trigger("scroll"); }, 100); } }); - + highlightReply(post_response.id); window.location.hash = post_response.id; $(window).scrollTop($(document).height()); - + $(form).find('input[type="submit"]').val(submit_txt); $(form).find('input[type="submit"]').removeAttr('disabled'); $(form).find('input[name="subject"],input[name="file_url"],\ @@ -132,10 +138,10 @@ $(window).ready(function() { contentType: false, processData: false }, 'json'); - + $(form).find('input[type="submit"]').val(_('Posting...')); $(form).find('input[type="submit"]').attr('disabled', true); - + return false; }); }; From d48ba6113a0415571476fd9c8422d23ebcbf79dc Mon Sep 17 00:00:00 2001 From: Zankaria Date: Sat, 8 Feb 2025 18:52:20 +0100 Subject: [PATCH 080/258] ajax.js: fix previous commit --- js/ajax.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/js/ajax.js b/js/ajax.js index d2319f39..0401cca7 100644 --- a/js/ajax.js +++ b/js/ajax.js @@ -19,16 +19,17 @@ $(window).ready(function() { // Enable submit button if disabled (cache problem) $('input[type="submit"]').removeAttr('disabled'); - // In the captcha is present, halt if it does not have a response.. - if (captcha_renderer && postCaptchaId && !captcha_renderer.hasResponse(postCaptchaId)) { - captcha_renderer.execute(postCaptchaId); - return false; - } - var setup_form = function($form) { $form.submit(function() { if (do_not_ajax) return true; + + // In the captcha is present, halt if it does not have a response.. + if (captcha_renderer && postCaptchaId && !captcha_renderer.hasResponse(postCaptchaId)) { + captcha_renderer.execute(postCaptchaId); + return false; + } + var form = this; var submit_txt = $(this).find('input[type="submit"]').val(); if (window.FormData === undefined) From 262e8971bdda56e82b557ffef72adff61921a479 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Sat, 8 Feb 2025 21:30:08 +0100 Subject: [PATCH 081/258] main.js: store captcha mode --- templates/main.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/templates/main.js b/templates/main.js index 574a3611..c4c71bc3 100755 --- a/templates/main.js +++ b/templates/main.js @@ -357,6 +357,8 @@ function onCaptchaLoad(renderer) { } {% if config.dynamic_captcha %} // If dynamic captcha +var captchaMode = 'dynamic'; + function isDynamicCaptchaEnabled() { let cookie = getCookie('captcha-required'); return cookie === '1'; @@ -370,8 +372,15 @@ function initCaptcha() { } } } +{% else %} +var captchaMode = 'static'; {% endif %} // End if dynamic captcha {% else %} // Else if any captcha +var captchaMode = 'none'; + +function isDynamicCaptchaEnabled() { + return false; +} // No-op for `init()`. function initCaptcha() {} {% endif %} // End if any captcha @@ -427,9 +436,11 @@ function doPost(form) { saved[document.location] = form.elements['body'].value; sessionStorage.body = JSON.stringify(saved); - if (captcha_renderer && postCaptchaId && !captcha_renderer.hasResponse(postCaptchaId)) { - captcha_renderer.execute(postCaptchaId); - return false; + if (captchaMode === 'static' || (captchaMode === 'dynamic' && isDynamicCaptchaEnabled())) { + if (captcha_renderer && postCaptchaId && !captcha_renderer.hasResponse(postCaptchaId)) { + captcha_renderer.execute(postCaptchaId); + return false; + } } // Needs to be delayed by at least 1 frame, otherwise it may reset the form (read captcha) fields before they're sent. From 91f53552c9a1efd5753865b79792a09d636f46e5 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Sat, 8 Feb 2025 21:30:25 +0100 Subject: [PATCH 082/258] ajax.js: trigger the captcha only if present and needed --- js/ajax.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/js/ajax.js b/js/ajax.js index 0401cca7..f65c6c96 100644 --- a/js/ajax.js +++ b/js/ajax.js @@ -24,10 +24,12 @@ $(window).ready(function() { if (do_not_ajax) return true; - // In the captcha is present, halt if it does not have a response.. - if (captcha_renderer && postCaptchaId && !captcha_renderer.hasResponse(postCaptchaId)) { - captcha_renderer.execute(postCaptchaId); - return false; + // If the captcha is present, halt if it does not have a response. + if (captchaMode === 'static' || (captchaMode === 'dynamic' && isDynamicCaptchaEnabled())) { + if (captcha_renderer && postCaptchaId && !captcha_renderer.hasResponse(postCaptchaId)) { + captcha_renderer.execute(postCaptchaId); + return false; + } } var form = this; From e677751010efcce330354b932bcbf9e5af0cbe64 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Sat, 8 Feb 2025 22:22:23 +0100 Subject: [PATCH 083/258] config.php: add youtube shorts embedding --- inc/config.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/inc/config.php b/inc/config.php index 53515a21..7d194034 100644 --- a/inc/config.php +++ b/inc/config.php @@ -1195,6 +1195,10 @@ // Custom embedding (YouTube, vimeo, etc.) // It's very important that you match the entire input (with ^ and $) or things will not work correctly. $config['embedding'] = array( + [ + '/^https?:\/\/(\w+\.)?youtube\.com\/shorts\/([a-zA-Z0-9\-_]{10,11})$/i', + '' + ], array( '/^https?:\/\/(\w+\.)?youtube\.com\/watch\?v=([a-zA-Z0-9\-_]{10,11})(&.+)?$/i', '' From da7266d5e1700f51298968c4a401b18cfdd8b802 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Sat, 8 Feb 2025 22:29:14 +0100 Subject: [PATCH 084/258] config.php: adjust embedding margins --- inc/config.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/inc/config.php b/inc/config.php index 7d194034..e9f94558 100644 --- a/inc/config.php +++ b/inc/config.php @@ -1197,7 +1197,7 @@ $config['embedding'] = array( [ '/^https?:\/\/(\w+\.)?youtube\.com\/shorts\/([a-zA-Z0-9\-_]{10,11})$/i', - '' + '' ], array( '/^https?:\/\/(\w+\.)?youtube\.com\/watch\?v=([a-zA-Z0-9\-_]{10,11})(&.+)?$/i', @@ -1221,15 +1221,15 @@ ), [ '/^https?:\/\/(\w+\.)?vocaroo\.com\/i\/([a-zA-Z0-9]{2,15})$/i', - '' + '' ], [ '/^https?:\/\/(\w+\.)?voca\.ro\/([a-zA-Z0-9]{2,15})$/i', - '' + '' ], [ '/^https?:\/\/(\w+\.)?vocaroo\.com\/([a-zA-Z0-9]{2,15})#?$/i', - '' + '' ] ); From 856d124c88ed617ada9e332793bff648caefe290 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Sat, 8 Feb 2025 22:40:38 +0100 Subject: [PATCH 085/258] config.php: adjust youtube shorts sizes --- inc/config.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/config.php b/inc/config.php index e9f94558..05e37a6e 100644 --- a/inc/config.php +++ b/inc/config.php @@ -1197,7 +1197,7 @@ $config['embedding'] = array( [ '/^https?:\/\/(\w+\.)?youtube\.com\/shorts\/([a-zA-Z0-9\-_]{10,11})$/i', - '' + '' ], array( '/^https?:\/\/(\w+\.)?youtube\.com\/watch\?v=([a-zA-Z0-9\-_]{10,11})(&.+)?$/i', From 6df32997bfd47a122e45f78976adaa3942b04154 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Sat, 8 Feb 2025 23:02:47 +0100 Subject: [PATCH 086/258] config.php: adjust youtube short referer policy (breaks on ids with a - dash) --- inc/config.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/config.php b/inc/config.php index 05e37a6e..e53a5aa7 100644 --- a/inc/config.php +++ b/inc/config.php @@ -1197,7 +1197,7 @@ $config['embedding'] = array( [ '/^https?:\/\/(\w+\.)?youtube\.com\/shorts\/([a-zA-Z0-9\-_]{10,11})$/i', - '' + '' ], array( '/^https?:\/\/(\w+\.)?youtube\.com\/watch\?v=([a-zA-Z0-9\-_]{10,11})(&.+)?$/i', From 4787e98c02cb972411f90d624ede31e60b8044d8 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 12 Feb 2025 20:05:55 +0100 Subject: [PATCH 087/258] youtube.js: rework --- js/youtube.js | 140 ++++++++++++++++++++++++++++---------------------- 1 file changed, 79 insertions(+), 61 deletions(-) diff --git a/js/youtube.js b/js/youtube.js index 608b5dba..5a7b6a42 100644 --- a/js/youtube.js +++ b/js/youtube.js @@ -1,41 +1,45 @@ /* -* youtube -* https://github.com/savetheinternet/Tinyboard/blob/master/js/youtube.js -* -* Don't load the YouTube player unless the video image is clicked. -* This increases performance issues when many videos are embedded on the same page. -* Currently only compatiable with YouTube. -* -* Proof of concept. -* -* Released under the MIT license -* Copyright (c) 2013 Michael Save -* Copyright (c) 2013-2014 Marcin Łabanowski -* -* Usage: -* $config['embedding'] = array(); -* $config['embedding'][0] = array( -* '/^https?:\/\/(\w+\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9\-_]{10,11})(&.+)?$/i', -* $config['youtube_js_html']); -* $config['additional_javascript'][] = 'js/jquery.min.js'; -* $config['additional_javascript'][] = 'js/youtube.js'; -* -*/ + * Don't load the 3rd party embedded content player unless the image is clicked. + * This increases performance issues when many videos are embedded on the same page. + * + * Released under the MIT license + * Copyright (c) 2013 Michael Save + * Copyright (c) 2013-2014 Marcin Łabanowski + * Copyright (c) 2025 Zankaria Auxa + * + * Usage: + * $config['embedding'] = array(); + * $config['embedding'][0] = array( + * '/^https?:\/\/(\w+\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9\-_]{10,11})(&.+)?$/i', + * $config['youtube_js_html']); + * $config['additional_javascript'][] = 'js/jquery.min.js'; + * $config['additional_javascript'][] = 'js/youtube.js'; + */ -$(document).ready(function(){ - // Adds Options panel item +$(document).ready(function() { + const ON = '[Remove]'; + const YOUTUBE = 'www.youtube.com'; + + function makeEmbedNode(embedHost, videoId, width, height) { + return $(`' - ], - array( '/^https?:\/\/(\w+\.)?youtube\.com\/watch\?v=([a-zA-Z0-9\-_]{10,11})(&.+)?$/i', - '' - ), + '
    + + + +
    ' + ], + [ + '/^https?:\/\/(\w+\.)?youtube\.com\/shorts\/([a-zA-Z0-9\-_]{10,11})(\?.*)?$/i', + '
    + + + +
    ' + ], array( '/^https?:\/\/(\w+\.)?vimeo\.com\/(\d{2,10})(\?.+)?$/i', '' @@ -2015,12 +2023,6 @@ // is the absolute maximum, because MySQL cannot handle table names greater than 64 characters. $config['board_regex'] = '[0-9a-zA-Z$_\x{0080}-\x{FFFF}]{1,58}'; - // Youtube.js embed HTML code - $config['youtube_js_html'] = ''; - // Slack Report Notification $config['slack'] = false; $config['slack_channel'] = ""; From d8cafc8fd86f6345a54f4a2c371df952c3945b4e Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 12 Feb 2025 20:13:41 +0100 Subject: [PATCH 089/258] config.php: better youtube embedding regex --- inc/config.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/config.php b/inc/config.php index 62a02c45..def2cd27 100644 --- a/inc/config.php +++ b/inc/config.php @@ -1196,7 +1196,7 @@ // It's very important that you match the entire input (with ^ and $) or things will not work correctly. $config['embedding'] = array( [ - '/^https?:\/\/(\w+\.)?youtube\.com\/watch\?v=([a-zA-Z0-9\-_]{10,11})(&.+)?$/i', + '/^(?:(?:https?:)?\/\/)?((?:www|m)\.)?(?:(?:youtube(?:-nocookie)?\.com|youtu\.be))(?:\/(?:[\w\-]+\?v=|embed\/|live\/|v\/)?)([\w\-]{11})((?:\?|\&)\S+)?$/i', '
    From aa18595adf5781332753898c352c636283153bad Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 12 Feb 2025 21:32:14 +0100 Subject: [PATCH 090/258] main.js: do not initialize captcha if not required by the mode --- templates/main.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/templates/main.js b/templates/main.js index c4c71bc3..c4025646 100755 --- a/templates/main.js +++ b/templates/main.js @@ -251,7 +251,9 @@ var postCaptchaId = null; {% if config.captcha.mode == 'hcaptcha' %} // If hcaptcha function onCaptchaLoadHcaptcha() { - if (captcha_renderer === null && (active_page === 'index' || active_page === 'catalog' || active_page === 'thread')) { + if ((captchaMode === 'static' || (captchaMode === 'dynamic' && isDynamicCaptchaEnabled())) + && captcha_renderer === null + && (active_page === 'index' || active_page === 'catalog' || active_page === 'thread')) { let renderer = { /** * @returns {object} Opaque widget id. @@ -296,7 +298,9 @@ window.onCaptchaLoadTurnstile_post_thread = function() { // Should be called by the captcha API when it's ready. Ugly I know... D: function onCaptchaLoadTurnstile(action) { - if (captcha_renderer === null && (active_page === 'index' || active_page === 'catalog' || active_page === 'thread')) { + if ((captchaMode === 'static' || (captchaMode === 'dynamic' && isDynamicCaptchaEnabled())) + && captcha_renderer === null + && (active_page === 'index' || active_page === 'catalog' || active_page === 'thread')) { let renderer = { /** * @returns {object} Opaque widget id. From 89d79a5278ef9c7f1dd873e597c4a8800d0a1cda Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 12 Feb 2025 21:36:18 +0100 Subject: [PATCH 091/258] youtube.js: remove broken onion proxy --- js/youtube.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/js/youtube.js b/js/youtube.js index 5a7b6a42..4c87b0aa 100644 --- a/js/youtube.js +++ b/js/youtube.js @@ -27,11 +27,7 @@ $(document).ready(function() { // Adds Options panel item. if (typeof localStorage.youtube_embed_proxy === 'undefined') { - if (location.hostname.includes('.onion')){ - localStorage.youtube_embed_proxy = 'tuberyps2pn6dor6h47brof3w2asmauahhk4ei42krugybzzzo55klad.onion'; - } else { - localStorage.youtube_embed_proxy = 'incogtube.com'; // Default value. - } + localStorage.youtube_embed_proxy = 'incogtube.com'; // Default value. } if (window.Options && Options.get_tab('general')) { From a73456c28397e412f7cc1ec7310fe3b48e6ec112 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 12 Feb 2025 21:57:17 +0100 Subject: [PATCH 092/258] overboards.php: update exclude list --- templates/themes/overboards/overboards.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/themes/overboards/overboards.php b/templates/themes/overboards/overboards.php index 3290d127..6396f7ed 100644 --- a/templates/themes/overboards/overboards.php +++ b/templates/themes/overboards/overboards.php @@ -12,21 +12,21 @@ 'title' => 'Overboard', 'uri' => 'overboard', 'subtitle' => '30 most recently bumped threads', - 'exclude' => array('assembly', 'assembly_archive', 'gulag'), + 'exclude' => [ 'gulag', 'roulette', 'roulette_archive' ], 'thread_limit' => $thread_limit, ), array( 'title' => 'SFW Overboard', 'uri' => 'sfw', 'subtitle' => '30 most recently bumped threads from work-safe boards', - 'exclude' => array('assembly', 'assembly_archive', 'gulag', 'b', 'siberia'), + 'exclude' => [ 'gulag', 'b', 'siberia', 'roulette', 'roulette_archive' ], 'thread_limit' => $thread_limit, ), array( 'title' => 'Alternate Overboard', 'uri' => 'alt', 'subtitle' => '30 most recently bumped threads from smaller interest boards', - 'exclude' => array('assembly', 'assembly_archive', 'gulag', 'leftypol', 'b', 'siberia', 'meta'), + 'exclude' => [ 'gulag', 'leftypol', 'b', 'siberia', 'meta', 'roulette', 'roulette_archive' ], 'thread_limit' => $thread_limit, ), ); From 9bb7f0d2f2abd51ecdeab6b11cb4bcdfde2bab0a Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 12 Feb 2025 23:25:43 +0100 Subject: [PATCH 093/258] general.js: do not move the style-select in the options --- js/options/general.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/js/options/general.js b/js/options/general.js index c0652269..6715ae1d 100644 --- a/js/options/general.js +++ b/js/options/general.js @@ -43,9 +43,6 @@ $(function(){ document.location.reload(); } }); - - - $("#style-select").detach().css({float:"none","margin-bottom":0}).appendTo(tab.content); }); }(); From aed49a6188a79d0a87e7c744761a052a105efcee Mon Sep 17 00:00:00 2001 From: Zankaria Date: Thu, 13 Feb 2025 00:00:20 +0100 Subject: [PATCH 094/258] ajax.js: clear embed --- js/ajax.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/ajax.js b/js/ajax.js index f65c6c96..3cb06bf1 100644 --- a/js/ajax.js +++ b/js/ajax.js @@ -111,7 +111,7 @@ $(window).ready(function() { $(form).find('input[type="submit"]').val(submit_txt); $(form).find('input[type="submit"]').removeAttr('disabled'); $(form).find('input[name="subject"],input[name="file_url"],\ - textarea[name="body"],input[type="file"]').val('').change(); + textarea[name="body"],input[type="file"],input[name="embed"]').val('').change(); }, cache: false, contentType: false, @@ -123,7 +123,7 @@ $(window).ready(function() { $(form).find('input[type="submit"]').val(submit_txt); $(form).find('input[type="submit"]').removeAttr('disabled'); $(form).find('input[name="subject"],input[name="file_url"],\ - textarea[name="body"],input[type="file"]').val('').change(); + textarea[name="body"],input[type="file"],input[name="embed"]').val('').change(); } else { alert(_('An unknown error occured when posting!')); $(form).find('input[type="submit"]').val(submit_txt); From 9746293fed591e34d250f42c976977f44d05ec5f Mon Sep 17 00:00:00 2001 From: Zankaria Date: Thu, 13 Feb 2025 10:35:01 +0100 Subject: [PATCH 095/258] inline-expanding.js: fit expanded images into the screen's height (port of soyjak party feature) --- js/inline-expanding.js | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/js/inline-expanding.js b/js/inline-expanding.js index 41625d2d..c44843b0 100644 --- a/js/inline-expanding.js +++ b/js/inline-expanding.js @@ -17,6 +17,10 @@ $(document).ready(function() { // Default maximum image loads. const DEFAULT_MAX = 5; + if (localStorage.inline_expand_fit_height !== 'false') { + $('').appendTo($('head')); + } + let inline_expand_post = function() { let link = this.getElementsByTagName('a'); @@ -56,12 +60,12 @@ $(document).ready(function() { }, add: function(ele) { ele.deferred = $.Deferred(); - ele.deferred.done(function () { + ele.deferred.done(function() { let $loadstart = $.Deferred(); let thumb = ele.childNodes[0]; let img = ele.childNodes[1]; - let onLoadStart = function (img) { + let onLoadStart = function(img) { if (img.naturalWidth) { $loadstart.resolve(img, thumb); } else { @@ -69,15 +73,15 @@ $(document).ready(function() { } }; - $(img).one('load', function () { - $.when($loadstart).done(function () { - // Once fully loaded, update the waiting queue. + $(img).one('load', function() { + $.when($loadstart).done(function() { + // once fully loaded, update the waiting queue --loading; $(ele).data('imageLoading', 'false'); update(); }); }); - $loadstart.done(function (img, thumb) { + $loadstart.done(function(img, thumb) { thumb.style.display = 'none'; img.style.display = ''; }); @@ -202,6 +206,8 @@ $(document).ready(function() { Options.extend_tab('general', '' + _('Number of simultaneous image downloads (0 to disable): ') + ''); + Options.extend_tab('general', ''); + $('#inline-expand-max input') .css('width', '50px') .val(localStorage.inline_expand_max || DEFAULT_MAX) @@ -212,6 +218,21 @@ $(document).ready(function() { localStorage.inline_expand_max = val; }); + + $('#inline-expand-fit-height input').on('change', function() { + if (localStorage.inline_expand_fit_height !== 'false') { + localStorage.inline_expand_fit_height = 'false'; + $('#expand-fit-height-style').remove(); + } + else { + localStorage.inline_expand_fit_height = 'true'; + $('').appendTo($('head')); + } + }); + + if (localStorage.inline_expand_fit_height !== 'false') { + $('#inline-expand-fit-height input').prop('checked', true); + } } if (window.jQuery) { From b1989a69b05aa05bdeb9d2d65b966bbf5e0c3731 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Fri, 14 Feb 2025 09:40:04 +0100 Subject: [PATCH 096/258] flags: add Alunya flag --- static/flags/alunya.png | Bin 0 -> 399 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 static/flags/alunya.png diff --git a/static/flags/alunya.png b/static/flags/alunya.png new file mode 100644 index 0000000000000000000000000000000000000000..799fd5099a77c7347a60957b6b3690c2ee745cd5 GIT binary patch literal 399 zcmV;A0dW3_P)bF5XNV>34t64sf099iB{sD2k^wkC$QJzKu{mRSFrUdd;&p)v#>l7E6HiRBA^ct zOd0pv>?FHk&mZi~?94Zl*=&v!4X=ZpZ3@HCiSdf#n4%~mXzaMaPcywoki_^{+bqkp z9g^p{wzghs&w#>Z#lV;A8f;FkjQ2li!jl52&Lr#r;8AVt7&k$!e0qH|VSnKeTQmD8 z+;;#Vz)P})z8Iz$jYi~L+gh4-3wtHfRuy}5Ns^GP>lzysYxyh+ZTX3OWq@-`S(c4+ z1PFd)8(Kd-?~36Aq%8Tpw$LAQ1XfZc{zW znp_@U({1x&b}hOAcLcT%F2tVkdeDS*V!WTyboS*;fh2P?pBs Date: Fri, 14 Feb 2025 12:48:58 +0100 Subject: [PATCH 097/258] UserPostQueries.php: fix paging boundaries --- inc/Data/UserPostQueries.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/inc/Data/UserPostQueries.php b/inc/Data/UserPostQueries.php index 8f803b93..1c203431 100644 --- a/inc/Data/UserPostQueries.php +++ b/inc/Data/UserPostQueries.php @@ -44,19 +44,20 @@ class UserPostQueries { $posts_count = \count($posts); + // By fetching one extra post bellow and/or above the limit, we know if there are any posts beside the current page. if ($posts_count === $page_size + 2) { $has_extra_prev_post = true; $has_extra_end_post = true; - } elseif ($posts_count === $page_size + 1) { - $has_extra_prev_post = $start_id !== null && $start_id === (int)$posts[0]['id']; - $has_extra_end_post = !$has_extra_prev_post; } else { - $has_extra_prev_post = false; - $has_extra_end_post = false; + /* + * If the id we start fetching from is also the first id fetched from the DB, then we exclude it from + * the results, noting that we fetched 1 more posts than we needed, and it was before the current page. + * Hence, we have no extra post at the end and no next page. + */ + $has_extra_prev_post = $start_id !== null && $start_id === (int)$posts[0]['id']; + $has_extra_end_post = !$has_extra_prev_post && $posts_count > $page_size; } - // Since we fetched one post bellow and/or above the limit, we always know if there are any posts after the current page. - // Get the previous cursor, if any. if ($has_extra_prev_post) { \array_shift($posts); From 9a5b79452f6a8f87849227d525721971b4d425c0 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Sun, 16 Feb 2025 23:52:31 +0100 Subject: [PATCH 098/258] pages.php: remove unused code --- inc/mod/pages.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/inc/mod/pages.php b/inc/mod/pages.php index 380a5627..52b4ee2c 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -1087,12 +1087,6 @@ function mod_user_posts_by_passwd(Context $ctx, string $passwd, string $encoded_ $args['boards'] = $boards; $args['token'] = make_secure_link_token('ban'); - if (empty($encoded_cursor)) { - $args['security_token'] = make_secure_link_token("user_posts/passwd/$passwd"); - } else { - $args['security_token'] = make_secure_link_token("user_posts/passwd/$passwd/cursor/$encoded_cursor"); - } - mod_page(\sprintf('%s: %s', _('Password'), \htmlspecialchars($passwd)), 'mod/view_passwd.html', $args); } From 2da6c95aa55ce6ed7cc44a06899acd85020867c0 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Sun, 16 Feb 2025 18:07:32 +0100 Subject: [PATCH 099/258] functions.php: remove unused parameter from markup --- inc/functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/functions.php b/inc/functions.php index 66cd6fa7..90079561 100644 --- a/inc/functions.php +++ b/inc/functions.php @@ -2069,7 +2069,7 @@ function remove_modifiers($body) { return preg_replace('@(.+?)@usm', '', $body); } -function markup(&$body, $track_cites = false, $op = false) { +function markup(&$body, $track_cites = false) { global $board, $config, $markup_urls; $modifiers = extract_modifiers($body); From c73a893e542ab5f91502605c57a77e2ed893919a Mon Sep 17 00:00:00 2001 From: Zankaria Date: Mon, 17 Feb 2025 00:31:59 +0100 Subject: [PATCH 100/258] functions.php: strikethrough bad citations --- inc/functions.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/inc/functions.php b/inc/functions.php index 90079561..def00287 100644 --- a/inc/functions.php +++ b/inc/functions.php @@ -2168,12 +2168,15 @@ function markup(&$body, $track_cites = false) { link_for(array('id' => $cite, 'thread' => $cited_posts[$cite])) . '#' . $cite . '">' . '>>' . $cite . ''; + } else { + $replacement = ">>$cite"; + } - $body = mb_substr_replace($body, $matches[1][0] . $replacement . $matches[3][0], $matches[0][1] + $skip_chars, mb_strlen($matches[0][0])); - $skip_chars += mb_strlen($matches[1][0] . $replacement . $matches[3][0]) - mb_strlen($matches[0][0]); + $body = mb_substr_replace($body, $matches[1][0] . $replacement . $matches[3][0], $matches[0][1] + $skip_chars, mb_strlen($matches[0][0])); + $skip_chars += mb_strlen($matches[1][0] . $replacement . $matches[3][0]) - mb_strlen($matches[0][0]); - if ($track_cites && $config['track_cites']) - $tracked_cites[] = array($board['uri'], $cite); + if ($track_cites && $config['track_cites']) { + $tracked_cites[] = array($board['uri'], $cite); } } } From 9a4ce56d8687c46d12ec619eee1a7459b76254b1 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Mon, 17 Feb 2025 17:21:32 +0100 Subject: [PATCH 101/258] dark_red.css: always use Verdana font if available --- stylesheets/dark_red.css | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/stylesheets/dark_red.css b/stylesheets/dark_red.css index 173a6619..74ef4e2b 100644 --- a/stylesheets/dark_red.css +++ b/stylesheets/dark_red.css @@ -33,11 +33,11 @@ div.title p { a, a:link, a:visited, .intro a.email span.name { color: #CCC; text-decoration: none; - font-family: sans-serif; + font-family: Verdana, sans-serif; } a:link:hover, a:visited:hover { color: #fff; - font-family: sans-serif; + font-family: Verdana, sans-serif; text-decoration: none; } a.post_no { @@ -79,7 +79,7 @@ div.post.inline { } .intro span.subject { font-size: 12px; - font-family: sans-serif; + font-family: Verdana, sans-serif; color: #446655; font-weight: 800; } @@ -100,7 +100,7 @@ input[type="text"], textarea, select { border: #666666 1px solid; padding-left: 5px; padding-right: -5px; - font-family: sans-serif; + font-family: Verdana, sans-serif; font-size: 10pt; } input[type="password"] { @@ -151,7 +151,7 @@ span.trip { } div.pages { background: #1E1E1E; - font-family: sans-serif; + font-family: Verdana, sans-serif; } .bar.bottom { bottom: 0px; From 0819c509faadff388f5f983e005d4f3da958a17c Mon Sep 17 00:00:00 2001 From: Zankaria Date: Mon, 17 Feb 2025 18:01:46 +0100 Subject: [PATCH 102/258] dark_red.css: increase contrast --- stylesheets/dark_red.css | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/stylesheets/dark_red.css b/stylesheets/dark_red.css index 74ef4e2b..4ffe4eeb 100644 --- a/stylesheets/dark_red.css +++ b/stylesheets/dark_red.css @@ -5,7 +5,7 @@ /*dark.css has been prepended (2021-11-11) instead of @import'd for performance*/ body { background: #1E1E1E; - color: #A7A7A7; + color: #C0C0C0; font-family: Verdana, sans-serif; font-size: 14px; } @@ -31,7 +31,7 @@ div.title p { font-size: 10px; } a, a:link, a:visited, .intro a.email span.name { - color: #CCC; + color: #EEE; text-decoration: none; font-family: Verdana, sans-serif; } @@ -48,11 +48,11 @@ a.post_no:hover { text-decoration: underline overline; } .intro a.post_no { - color: #CCC; + color: #EEE; } div.post.reply { background: #333333; - border: #555555 1px solid; + border: #4f4f4f 1px solid; @media (max-width: 48em) { border-left-style: none; @@ -60,7 +60,7 @@ div.post.reply { } } div.post.reply.highlighted { - background: #555; + background: #4f4f4f; border: transparent 1px solid; @media (max-width: 48em) { @@ -69,13 +69,13 @@ div.post.reply.highlighted { } } div.post.reply div.body a:link, div.post.reply div.body a:visited { - color: #CCCCCC; + color: #EEE; } div.post.reply div.body a:link:hover, div.post.reply div.body a:visited:hover { color: #32DD72; } div.post.inline { - border: #555555 1px solid; + border: #4f4f4f 1px solid; } .intro span.subject { font-size: 12px; @@ -96,7 +96,7 @@ div.post.inline { } input[type="text"], textarea, select { background: #333333; - color: #CCCCCC; + color: #EEE; border: #666666 1px solid; padding-left: 5px; padding-right: -5px; @@ -105,7 +105,7 @@ input[type="text"], textarea, select { } input[type="password"] { background: #333333; - color: #CCCCCC; + color: #EEE; border: #666666 1px solid; } form table tr th { @@ -133,10 +133,10 @@ div.banner a { input[type="submit"] { background: #333333; border: #888888 1px solid; - color: #CCCCCC; + color: #EEE; } input[type="submit"]:hover { - background: #555555; + background: #4f4f4f; border: #888888 1px solid; color: #32DD72; } @@ -159,7 +159,7 @@ div.pages { background-color: #1E1E1E; } div.pages a.selected { - color: #CCCCCC; + color: #EEE; } hr { height: 1px; @@ -167,7 +167,7 @@ hr { } div.boardlist { text-align: center; - color: #A7A7A7; + color: #C0C0C0; } div.ban { background-color: transparent; @@ -188,7 +188,7 @@ div.boardlist:not(.bottom) { } .desktop-style div.boardlist:not(.bottom) { text-shadow: black 1px 1px 1px, black -1px -1px 1px, black -1px 1px 1px, black 1px -1px 1px; - color: #A7A7A7; + color: #C0C0C0; background-color: #1E1E1E; } div.report { @@ -211,7 +211,7 @@ div.report { } .box { background: #333333; - border-color: #555555; + border-color: #4f4f4f; color: #C5C8C6; border-radius: 10px; } @@ -221,7 +221,7 @@ div.report { } table thead th { background: #333333; - border-color: #555555; + border-color: #4f4f4f; color: #C5C8C6; border-radius: 4px; } @@ -229,11 +229,11 @@ table tbody tr:nth-of-type( even ) { background-color: #333333; } table.board-list-table .board-uri .board-sfw { - color: #CCCCCC; + color: #EEE; } tbody.board-list-omitted td { background: #333333; - border-color: #555555; + border-color: #4f4f4f; } table.board-list-table .board-tags .board-cell:hover { background: #1e1e1e; From 9256c15c4657b62009f0cafb5b2290b6f2e651cd Mon Sep 17 00:00:00 2001 From: Zankaria Date: Mon, 17 Feb 2025 18:51:24 +0100 Subject: [PATCH 103/258] rules.html: extend rule 10 to extreme fetishes --- templates/rules.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/rules.html b/templates/rules.html index e152d682..9eaa3c3b 100644 --- a/templates/rules.html +++ b/templates/rules.html @@ -7,7 +7,7 @@

    - RULES + RULES

    GLOBAL:

    @@ -36,7 +36,7 @@ Opening posts with liberalism or reactionary topics will be treated with far mor

    9) Due to derailing, COVID denialism outside the COVID-19 thread will be deleted.

    -

    10) All boards except for /siberia/ (and potentially /roulette/) are 'Safe For Work' boards. Pornography should not be posted on them without good reason, and any pornography on these boards should be hidden using the Spoiler Image option. New threads on /siberia/ with pornographic topics should have a Spoiler Image on the opening post.

    +

    10) All boards except for /siberia/ (and potentially /roulette/) are 'Safe For Work' boards. Pornography should not be posted on them without good reason, and any pornography on these boards should be hidden using the Spoiler Image option. New threads on /siberia/ with pornographic topics should have a Spoiler Image on the opening post. Some kinds of pornographic content are always banned on every board, including /siberia/: cp/loli/jailbait/anything that could possibly interpreted a child, "feral" furry, zoophilia, murder/gore (photographic) and other suitably extreme fetishes.

    11) Posts should, overall, be conductive to an informed and productive discussion. /leftypol/ is not an academic journal, but it also should not be a cesspit of back and forth bickering and pointless insults. Users should attempt to argue for the point they are presenting in an honest and open way and should be receptive to information or arguments that do, in fact, challenge their views.

    From e5ba5feb25e6d7e7a4b47c673a321d82c23865b0 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Mon, 17 Feb 2025 21:42:20 +0100 Subject: [PATCH 104/258] youtube.js: adjust referrer policy --- js/youtube.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/youtube.js b/js/youtube.js index 4c87b0aa..4a5a5afe 100644 --- a/js/youtube.js +++ b/js/youtube.js @@ -22,7 +22,7 @@ $(document).ready(function() { function makeEmbedNode(embedHost, videoId, width, height) { return $(`
    -
    +
    {{ antibot.html() }}