diff --git a/inc/mod/pages.php b/inc/mod/pages.php index 1bbe60d0..086ec6e1 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -6,6 +6,9 @@ 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 { @@ -868,7 +871,7 @@ function mod_ip_remove_note($ip, $id) { header('Location: ?/IP/' . $ip . '#notes', true, $config['redirect_http']); } -function mod_page_ip($ip) { +function mod_page_ip($ip, string $encoded_cursor = '') { global $config, $mod; if (filter_var($ip, FILTER_VALIDATE_IP) === false) @@ -910,33 +913,103 @@ function mod_page_ip($ip) { 'posts' => [] ]; - if ($config['mod']['dns_lookup']) + if ($config['mod']['dns_lookup']) { $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(); 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)); + $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; + } + } - while ($post = $query->fetch(PDO::FETCH_ASSOC)) { - if (!$post['thread']) { - $po = new Thread($post, '?/', $mod, false); + 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 { - $po = new Post($post, '?/', $mod); + throw new RuntimeException("Unknown cursor type '$cursor_type'"); } - if (!isset($args['posts'][$board['uri']])) { - $args['posts'][$board['uri']] = [ 'board' => $board, 'posts' => [] ]; + $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); } - $args['posts'][$board['uri']]['posts'][] = $po->build(true); } } + // 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; + $args['boards'] = $boards; $args['token'] = make_secure_link_token('ban');