From 223a9713cb05f57b56b202556c6cb00418827db1 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Mon, 14 Oct 2024 23:12:27 +0200 Subject: [PATCH 01/16] net.php: add functions to encode and decode a typed cursor over an url --- inc/functions/net.php | 60 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/inc/functions/net.php b/inc/functions/net.php index e1541046..49563ecf 100644 --- a/inc/functions/net.php +++ b/inc/functions/net.php @@ -14,3 +14,63 @@ function is_connection_secure(bool $trust_headers): bool { } return false; } + +/** + * Encodes a string into a base64 variant without characters illegal in urls. + */ +function base64_url_encode(string $input): string { + return str_replace([ '+', '/', '=' ], [ '-', '_', '' ], base64_encode($input)); +} + +/** + * Decodes a string from a base64 variant without characters illegal in urls. + */ +function base64_url_decode(string $input): string { + return base64_decode(strtr($input, '-_', '+/')); +} + +/** + * Encodes a typed cursor. + * + * @param string $type The type for the cursor. Only the first character is considered. + * @param array $map A map of key-value pairs to encode. + * @return string An encoded string that can be sent through urls. Empty if either parameter is empty. + */ +function encode_cursor(string $type, array $map): string { + if (empty($type) || empty($map)) { + return ''; + } + + $acc = $type[0]; + foreach ($map as $key => $value) { + $acc .= "|$key#$value"; + } + return base64_url_encode($acc); +} + +/** + * Decodes a typed cursor. + * + * @param string $cursor A string emitted by `encode_cursor`. + * @return array An array with the type of the cursor and an array of key-value pairs. The type is null and the map + * empty if either there are no key-value pairs or the encoding is incorrect. + */ +function decode_cursor(string $cursor): array { + $map = []; + $type = ''; + $acc = base64_url_decode($cursor); + if ($acc === false || empty($acc)) { + return [ null, [] ]; + } + + $type = $acc[0]; + foreach (explode('|', substr($acc, 2)) as $pair) { + $pair = explode('#', $pair); + if (count($pair) >= 2) { + $key = $pair[0]; + $value = $pair[1]; + $map[$key] = $value; + } + } + return [ $type, $map ]; +} From 388dce77186bb1c166fac6a68e8f0f9251e0dc23 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Tue, 15 Oct 2024 00:15:54 +0200 Subject: [PATCH 02/16] view_ip.html: add links to previous and next cursors with mod_user_posts_by_ip --- templates/mod/view_ip.html | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/templates/mod/view_ip.html b/templates/mod/view_ip.html index 867312c4..958d75f0 100644 --- a/templates/mod/view_ip.html +++ b/templates/mod/view_ip.html @@ -18,7 +18,7 @@ {{ notes_length }} {% trans %}note on record{% plural notes_length %}notes on record{% endtrans %} - {% if notes|length > 0 %} + {% if notes and notes|length > 0 %} @@ -146,3 +146,12 @@
{% trans 'Staff' %}
{% endif %} +
+ [Page 1] + {% if cursor_prev %} + [Previous Page] + {% endif %} + {% if cursor_next %} + [Next Page] + {% endif %} +
From 4023840aadb990b12170d146f64acea397838f2f Mon Sep 17 00:00:00 2001 From: Zankaria Date: Tue, 15 Oct 2024 18:35:32 +0200 Subject: [PATCH 03/16] UserPostQueries.php: add user post queries class --- inc/Data/PageFetchResult.php | 15 ++++ inc/Data/UserPostQueries.php | 159 +++++++++++++++++++++++++++++++++++ 2 files changed, 174 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; + } + + 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); + } 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. + $start_id = null; + if ($cursor_type !== null && isset($uri_id_cursor_map[$uri])) { + $value = $uri_id_cursor_map[$uri]; + if (\is_numeric($value)) { + $start_id = (int)$value; + } + } + + $posts = $callback($uri, $cursor_type, $start_id, $page_size); + + $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; + } else { + /* + * 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; + } + + // 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; + } + + /** + * 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'"); + } + }); + } + + /** + * 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` = :password AND `id` >= :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 f88c8fcd932ec3b5023889aaadd9d8d985ab4415 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Tue, 15 Oct 2024 19:00:05 +0200 Subject: [PATCH 04/16] pages.php: use UserPostQueries for mod_page_ip --- inc/mod/pages.php | 134 ++++++++++++++++++++++++++++++---------------- 1 file changed, 87 insertions(+), 47 deletions(-) diff --git a/inc/mod/pages.php b/inc/mod/pages.php index 9fcfc7c3..6d9eb8d4 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -3,10 +3,9 @@ * Copyright (c) 2010-2013 Tinyboard Development Group */ use Vichan\Context; -use Vichan\Data\{IpNoteQueries, ReportQueries}; -use Vichan\Functions\Format; -use Vichan\Functions\Net; -use Vichan\Data\Driver\CacheDriver; +use Vichan\Data\{IpNoteQueries, UserPostQueries, ReportQueries}; +use Vichan\Functions\{Format, Net}; +use Vichan\Data\Driver\{CacheDriver, LogDriver}; defined('TINYBOARD') or exit; @@ -897,20 +896,18 @@ function mod_ip_remove_note(Context $ctx, $cloaked_ip, $id) { error("Note $id does not exist for $cloaked_ip"); } - modLog("Removed a note for {$cloaked_ip}"); + modLog("Removed a note for {$cloaked_ip}"); - header('Location: ?/IP/' . $cloaked_ip . '#notes', true, $config['redirect_http']); + header('Location: ?/user_posts/ip/' . $cloaked_ip . '#notes', true, $config['redirect_http']); } - - function mod_ip(Context $ctx, $cip) { $ip = uncloak_ip($cip); global $mod; $config = $ctx->get('config'); 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'])) @@ -918,7 +915,7 @@ function mod_ip(Context $ctx, $cip) { Bans::delete($_POST['ban_id'], true, $mod['boards']); - header('Location: ?/IP/' . $cip . '#bans', true, $config['redirect_http']); + header("Location: ?/user_posts/ip/$cip#bans", true, $config['redirect_http']); return; } @@ -940,45 +937,42 @@ function mod_ip(Context $ctx, $cip) { $note_queries = $ctx->get(IpNoteQueries::class); $note_queries->add($ip, $mod['id'], $_POST['note']); - modLog("Added a note for {$cip}"); + modLog("Added a note for {$cip}"); - header('Location: ?/IP/' . $cip . '#notes', true, $config['redirect_http']); + header("Location: ?/user_posts/ip/$cip#notes", true, $config['redirect_http']); return; } + \header("Location: ?/user_posts/ip/$cip", true, $config['redirect_http']); +} - $args = []; - $args['ip'] = $ip; - $args['posts'] = []; +function mod_user_posts_by_ip(Context $ctx, string $cip, ?string $encoded_cursor = null) { + global $mod; - if ($config['mod']['dns_lookup'] && empty($config['ipcrypt_key'])) - $args['hostname'] = rDNS($ip); + $ip = uncloak_ip($cip); - $boards = listBoards(); - foreach ($boards as $board) { - openBoard($board['uri']); - if (!hasPermission($config['mod']['show_ip'], $board['uri'])) - continue; - $query = prepare(sprintf('SELECT * FROM ``posts_%s`` WHERE `ip` = :ip ORDER BY `sticky` DESC, `id` DESC LIMIT :limit', $board['uri'])); - $query->bindValue(':ip', $ip); - $query->bindValue(':limit', $config['mod']['ip_recentposts'], PDO::PARAM_INT); - $query->execute() or error(db_error($query)); - - while ($post = $query->fetch(PDO::FETCH_ASSOC)) { - if (!$post['thread']) { - $po = new Thread($post, '?/', $mod, false); - } else { - $po = new Post($post, '?/', $mod); - } - - if (!isset($args['posts'][$board['uri']])) - $args['posts'][$board['uri']] = array('board' => $board, 'posts' => []); - $args['posts'][$board['uri']]['posts'][] = $po->build(true); - } + if (\filter_var($ip, \FILTER_VALIDATE_IP) === false){ + error('Invalid IP address.'); } - $args['boards'] = $boards; - $args['token'] = make_secure_link_token('ban'); + $config = $ctx->get('config'); + + $args = [ + 'ip' => $ip, + 'posts' => [] + ]; + + if (isset($config['mod']['ip_recentposts'])) { + $log = $ctx->get(LogDriver::class); + $log->log(LogDriver::NOTICE, "'ip_recentposts' has been deprecated. Please use 'recent_user_posts' instead"); + $page_size = $config['mod']['ip_recentposts']; + } else { + $page_size = $config['mod']['recent_user_posts']; + } + + if ($config['mod']['dns_lookup'] && empty($config['ipcrypt_key'])) { + $args['hostname'] = rDNS($ip); + } if (hasPermission($config['mod']['view_ban'])) { $args['bans'] = Bans::find($ip, false, true, null, $config['auto_maintenance']); @@ -992,15 +986,61 @@ function mod_ip(Context $ctx, $cip) { } if (hasPermission($config['mod']['modlog_ip'])) { - $query = prepare("SELECT `username`, `mod`, `ip`, `board`, `time`, `text` FROM ``modlogs`` LEFT JOIN ``mods`` ON `mod` = ``mods``.`id` WHERE `text` LIKE :search ORDER BY `time` DESC LIMIT 50"); - $query->bindValue(':search', '%' . $cip . '%'); - $query->execute() or error(db_error($query)); - $args['logs'] = $query->fetchAll(PDO::FETCH_ASSOC); + $ret = Cache::get("mod_page_ip_modlog_ip_$ip"); + if (!$ret) { + $query = prepare("SELECT `username`, `mod`, `ip`, `board`, `time`, `text` FROM ``modlogs`` LEFT JOIN ``mods`` ON `mod` = ``mods``.`id` WHERE `text` LIKE :search ORDER BY `time` DESC LIMIT 50"); + $query->bindValue(':search', '%' . $ip . '%'); + $query->execute() or error(db_error($query)); + $ret = $query->fetchAll(PDO::FETCH_ASSOC); + Cache::set("mod_page_ip_modlog_ip_$ip", $ret, 900); + } + $args['logs'] = $ret; } else { $args['logs'] = []; } - $args['security_token'] = make_secure_link_token('IP/' . $cip); + $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, $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; + // Needed to create new bans. + $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. + $args['security_token'] = make_secure_link_token("IP/$cip"); mod_page(sprintf('%s: %s', _('IP'), htmlspecialchars($cip)), $config['file_mod_view_ip'], $args, $mod, $args['hostname']); } @@ -1957,7 +1997,7 @@ function mod_deletebyip(Context $ctx, $boardName, $post, $global = false) { // Record the action $cip = cloak_ip($ip); - modLog("Deleted all posts by IP address: $cip"); + modLog("Deleted all posts by IP address: $cip"); // Redirect header('Location: ?/' . sprintf($config['board_path'], $boardName) . $config['file_index'], true, $config['redirect_http']); @@ -2544,7 +2584,7 @@ function mod_report_dismiss(Context $ctx, $id, $action) { $report_queries->deleteByIp($ip); $cip = cloak_ip($ip); - modLog("Dismissed all reports by $cip"); + modLog("Dismissed all reports by $cip"); break; case '': default: From 4f9bb235f679cd180c29f856cffd6fe74a7e7218 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 11 Dec 2024 17:31:57 +0100 Subject: [PATCH 05/16] context.php: use UserPostQueries --- inc/context.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/inc/context.php b/inc/context.php index f8b00f35..81383783 100644 --- a/inc/context.php +++ b/inc/context.php @@ -2,7 +2,7 @@ namespace Vichan; use Vichan\Data\Driver\{CacheDriver, HttpDriver, ErrorLogLogDriver, FileLogDriver, LogDriver, StderrLogDriver, SyslogLogDriver}; -use Vichan\Data\{IpNoteQueries, ReportQueries}; +use Vichan\Data\{IpNoteQueries, UserPostQueries, ReportQueries}; use Vichan\Service\HCaptchaQuery; use Vichan\Service\SecureImageCaptchaQuery; use Vichan\Service\ReCaptchaQuery; @@ -108,5 +108,6 @@ function build_context(array $config): Context { return new ReportQueries($pdo, $auto_maintenance); }, IpNoteQueries::class => fn($c) => new IpNoteQueries($c->get(\PDO::class), $c->get(CacheDriver::class)), + UserPostQueries::class => fn($c) => new UserPostQueries($c->get(\PDO::class)) ]); } From 3c1b94d261b1c85fb10af0532500e34cfff81f45 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 18 Dec 2024 22:26:55 +0100 Subject: [PATCH 06/16] ip.html: use mod_user_posts_by_ip --- 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 d3af1b71..76454d20 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|cloak_ip }}] + [{{ post.ip|cloak_ip }}] {% endif %} From ad9916d4283afd98019eb05e802176f307aa3098 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Thu, 13 Feb 2025 11:41:38 +0100 Subject: [PATCH 07/16] 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 921d9bdb..9debe9fa 100644 --- a/mod.php +++ b/mod.php @@ -99,6 +99,9 @@ class Router { '/IP/([\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 8d9ef728c066c80526acf1571c6048035b9b29d9 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 18 Dec 2024 23:48:07 +0100 Subject: [PATCH 08/16] user_posts_list.html: add template to list posts --- templates/mod/user_posts_list.html | 16 ++++++++++++++++ 1 file changed, 16 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..42ce4a68 --- /dev/null +++ b/templates/mod/user_posts_list.html @@ -0,0 +1,16 @@ +{% if posts|length == 0 %} +

{% trans %}There are no posts.{% endtrans %}

+{% else %} + {% for board_posts in posts %} +
+ + + {{ config.board_abbreviation|sprintf(board_posts.board.uri) }} + - + {{ board_posts.board.title|e }} + + + {{ board_posts.posts|join('
') }} +
+ {% endfor %} +{% endif %} From c0ce478bea1c13b15f88dcdac6bf349ec2211eb4 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 18 Dec 2024 23:48:22 +0100 Subject: [PATCH 09/16] view_ip.html: use user_posts_list.html --- templates/mod/view_ip.html | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/templates/mod/view_ip.html b/templates/mod/view_ip.html index 958d75f0..7dbdf8a2 100644 --- a/templates/mod/view_ip.html +++ b/templates/mod/view_ip.html @@ -1,17 +1,4 @@ -{% for board_posts in posts %} -
- - - {{ config.board_abbreviation|sprintf(board_posts.board.uri) }} - - - {{ board_posts.board.title|e }} - - - {{ board_posts.posts|join('
') }} -
-{% endfor %} - -{% if mod|hasPermission(config.mod.view_notes) %} +{% if mod|hasPermission(config.mod.view_notes) and notes is not null %}
{% set notes_length = notes|length %} @@ -146,6 +133,8 @@
{% endif %} + +{{ include('mod/user_posts_list.html', {posts: posts}) }}
[Page 1] {% if cursor_prev %} From 542fd6ab40fa2553f51079631a6b875b4c7ba29d Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 18 Dec 2024 23:55:40 +0100 Subject: [PATCH 10/16] ip.html: add password link --- templates/post/ip.html | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/post/ip.html b/templates/post/ip.html index 76454d20..2ad7baca 100644 --- a/templates/post/ip.html +++ b/templates/post/ip.html @@ -1,3 +1,4 @@ {% if post.mod and post.mod|hasPermission(config.mod.show_ip, board.uri) %} [{{ post.ip|cloak_ip }}] + [{{ post.password[:15] }}] {% endif %} From d24add1b671cf45b1605155fdd1ec23ef2646764 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Thu, 13 Feb 2025 11:46:06 +0100 Subject: [PATCH 11/16] 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 9debe9fa..22450379 100644 --- a/mod.php +++ b/mod.php @@ -102,6 +102,9 @@ class Router { '/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 e29fd480ab463b1aec964592c073185d825092cd Mon Sep 17 00:00:00 2001 From: Zankaria Date: Thu, 19 Dec 2024 00:09:18 +0100 Subject: [PATCH 12/16] 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 463ba67e..93cd0a2c 100644 --- a/inc/config.php +++ b/inc/config.php @@ -1605,8 +1605,8 @@ 'default' => '/', // Do DNS lookups on IP addresses to get their hostname for the moderator IP pages (?/IP/x.x.x.x). 'dns_lookup' => true, - // How many recent posts, per board, to show in ?/IP/x.x.x.x. - 'ip_recentposts' => 5, + // How many recent posts, per board, to show in ?/user_posts/ip/x.x.x.x. and ?/user_posts/passwd/xxxxxxxx + 'recent_user_posts' => 5, // Number of posts to display on the reports page. 'recent_reports' => 10, // Number of actions to show per page in the moderation log. From 04a9f138aa6bc505adfcd0e2f66016ee83aae978 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Thu, 19 Dec 2024 00:16:10 +0100 Subject: [PATCH 13/16] pages.php: add mod_user_posts_by_passwd implementation --- inc/mod/pages.php | 64 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/inc/mod/pages.php b/inc/mod/pages.php index 6d9eb8d4..d15f96fa 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -1045,6 +1045,70 @@ function mod_user_posts_by_ip(Context $ctx, string $cip, ?string $encoded_cursor mod_page(sprintf('%s: %s', _('IP'), htmlspecialchars($cip)), $config['file_mod_view_ip'], $args, $mod, $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'])) { + $log = $ctx->get(LogDriver::class); + $log->log(LogDriver::NOTICE, "'ip_recentposts' has been deprecated. Please use 'recent_user_posts' instead"); + $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; + + mod_page(\sprintf('%s: %s', _('Password'), \htmlspecialchars(substr($passwd, 0, 15))), 'mod/view_passwd.html', $args, $mod); +} + function mod_edit_ban(Context $ctx, $ban_id) { global $mod; $config = $ctx->get('config'); From 6acfbfc9b351a9a9ce1d410b2cfa79be744946be Mon Sep 17 00:00:00 2001 From: Zankaria Date: Fri, 27 Dec 2024 19:13:27 +0100 Subject: [PATCH 14/16] view_passwd.html: add view password template --- templates/mod/view_passwd.html | 10 ++++++++++ 1 file changed, 10 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..72e3d747 --- /dev/null +++ b/templates/mod/view_passwd.html @@ -0,0 +1,10 @@ +{{ include('mod/user_posts_list.html', {posts: posts}) }} +
+ [Page 1] + {% if cursor_prev %} + [Previous Page] + {% endif %} + {% if cursor_next %} + [Next Page] + {% endif %} +
From 2bb7611c5e0eb28152c7e8082c1a6b7651ae2317 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Sun, 29 Dec 2024 00:19:48 +0100 Subject: [PATCH 15/16] 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 7dbdf8a2..330f493f 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) %} -
+ @@ -75,7 +75,7 @@ {{ bans_length }} {% trans %}ban on record{% plural notes_length %}bans on record{% endtrans %} {% for ban in bans %} - + {% include 'mod/ban_history.html' %} From 7a28ea552db1821a019924ed8c16c492c94230b6 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Mon, 17 Feb 2025 00:03:58 +0100 Subject: [PATCH 16/16] view_ip.html: change style --- templates/mod/view_ip.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/mod/view_ip.html b/templates/mod/view_ip.html index 330f493f..9aa1cd20 100644 --- a/templates/mod/view_ip.html +++ b/templates/mod/view_ip.html @@ -135,7 +135,7 @@ {% endif %} {{ include('mod/user_posts_list.html', {posts: posts}) }} -
+
[Page 1] {% if cursor_prev %} [Previous Page]