From 71416afc75cab86692560f3d454f924b23a7adbe Mon Sep 17 00:00:00 2001 From: Zankaria Date: Tue, 15 Oct 2024 18:35:32 +0200 Subject: [PATCH 1/3] 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 2/3] 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 3/3] 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)); } ]); }