forked from leftypol/leftypol
Merge branch '17-paginate-ips' into 'config'
Add IP pagination Closes #17 See merge request leftypol/leftypol!3
This commit is contained in:
commit
1a0054967b
6 changed files with 180 additions and 24 deletions
|
@ -1499,7 +1499,7 @@
|
||||||
|
|
||||||
// Do DNS lookups on IP addresses to get their hostname for the moderator IP pages (?/IP/x.x.x.x).
|
// 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;
|
$config['mod']['dns_lookup'] = true;
|
||||||
// How many recent posts, per board, to show in ?/IP/x.x.x.x.
|
// How many recent posts, per board, to show in each page of ?/IP/x.x.x.x.
|
||||||
$config['mod']['ip_recentposts'] = 5;
|
$config['mod']['ip_recentposts'] = 5;
|
||||||
|
|
||||||
// Number of posts to display on the reports page.
|
// Number of posts to display on the reports page.
|
||||||
|
|
|
@ -72,6 +72,7 @@ function sql_open() {
|
||||||
try {
|
try {
|
||||||
$options = [
|
$options = [
|
||||||
PDO::ATTR_TIMEOUT => $config['db']['timeout'],
|
PDO::ATTR_TIMEOUT => $config['db']['timeout'],
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // Set a consistent error mode between PHP versions.
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($config['db']['type'] == "mysql")
|
if ($config['db']['type'] == "mysql")
|
||||||
|
|
|
@ -15,3 +15,63 @@ function is_connection_https(): bool {
|
||||||
function is_connection_secure(): bool {
|
function is_connection_secure(): bool {
|
||||||
return is_connection_https() || (!empty($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] === '127.0.0.1');
|
return is_connection_https() || (!empty($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] === '127.0.0.1');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 ];
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,9 @@
|
||||||
|
|
||||||
use Vichan\Functions\Net;
|
use Vichan\Functions\Net;
|
||||||
|
|
||||||
|
use function Vichan\Functions\Net\decode_cursor;
|
||||||
|
use function Vichan\Functions\Net\encode_cursor;
|
||||||
|
|
||||||
defined('TINYBOARD') or exit;
|
defined('TINYBOARD') or exit;
|
||||||
|
|
||||||
function _link_or_copy(string $target, string $link): bool {
|
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']);
|
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;
|
global $config, $mod;
|
||||||
|
|
||||||
if (filter_var($ip, FILTER_VALIDATE_IP) === false)
|
if (filter_var($ip, FILTER_VALIDATE_IP) === false)
|
||||||
|
@ -880,7 +883,11 @@ function mod_page_ip($ip) {
|
||||||
|
|
||||||
Bans::delete($_POST['ban_id'], true, $mod['boards']);
|
Bans::delete($_POST['ban_id'], true, $mod['boards']);
|
||||||
|
|
||||||
header('Location: ?/IP/' . $ip . '#bans', true, $config['redirect_http']);
|
if (empty($encoded_cursor)) {
|
||||||
|
header("Location: ?/IP/$ip#bans", true, $config['redirect_http']);
|
||||||
|
} else {
|
||||||
|
header("Location: ?/IP/$ip/cursor/$encoded_cursor#bans", true, $config['redirect_http']);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -901,7 +908,11 @@ function mod_page_ip($ip) {
|
||||||
|
|
||||||
modLog("Added a note for <a href=\"?/IP/{$ip}\">{$ip}</a>");
|
modLog("Added a note for <a href=\"?/IP/{$ip}\">{$ip}</a>");
|
||||||
|
|
||||||
header('Location: ?/IP/' . $ip . '#notes', true, $config['redirect_http']);
|
if (empty($encoded_cursor)) {
|
||||||
|
header("Location: ?/IP/$ip#notes", true, $config['redirect_http']);
|
||||||
|
} else {
|
||||||
|
header("Location: ?/IP/$ip/cursor/$encoded_cursor#notes", true, $config['redirect_http']);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -910,33 +921,103 @@ function mod_page_ip($ip) {
|
||||||
'posts' => []
|
'posts' => []
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($config['mod']['dns_lookup'])
|
if ($config['mod']['dns_lookup']) {
|
||||||
$args['hostname'] = rDNS($ip);
|
$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();
|
$boards = listBoards();
|
||||||
foreach ($boards as $board) {
|
foreach ($boards as $board) {
|
||||||
openBoard($board['uri']);
|
$uri = $board['uri'];
|
||||||
if (!hasPermission($config['mod']['show_ip'], $board['uri']))
|
openBoard($uri);
|
||||||
continue;
|
if (hasPermission($config['mod']['show_ip'], $uri)) {
|
||||||
$query = prepare(sprintf('SELECT * FROM ``posts_%s`` WHERE `ip` = :ip ORDER BY `sticky` DESC, `id` DESC LIMIT :limit', $board['uri']));
|
// Extract the cursor relative to the board.
|
||||||
$query->bindValue(':ip', $ip);
|
$id_cursor = false;
|
||||||
$query->bindValue(':limit', $config['mod']['ip_recentposts'], PDO::PARAM_INT);
|
if (isset($board_id_cursor_map[$uri])) {
|
||||||
$query->execute() or error(db_error($query));
|
$value = $board_id_cursor_map[$uri];
|
||||||
|
if (is_numeric($value)) {
|
||||||
|
$id_cursor = (int)$value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
while ($post = $query->fetch(PDO::FETCH_ASSOC)) {
|
if ($id_cursor === false) {
|
||||||
if (!$post['thread']) {
|
$query = prepare(sprintf('SELECT * FROM `posts_%s` WHERE `ip` = :ip ORDER BY `sticky` DESC, `id` DESC LIMIT :limit', $uri));
|
||||||
$po = new Thread($post, '?/', $mod, false);
|
$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 {
|
} else {
|
||||||
$po = new Post($post, '?/', $mod);
|
throw new RuntimeException("Unknown cursor type '$cursor_type'");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isset($args['posts'][$board['uri']])) {
|
$posts_count = count($posts);
|
||||||
$args['posts'][$board['uri']] = [ 'board' => $board, '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['boards'] = $boards;
|
||||||
$args['token'] = make_secure_link_token('ban');
|
$args['token'] = make_secure_link_token('ban');
|
||||||
|
|
||||||
|
@ -970,7 +1051,11 @@ function mod_page_ip($ip) {
|
||||||
$args['logs'] = [];
|
$args['logs'] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$args['security_token'] = make_secure_link_token('IP/' . $ip);
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
mod_page(sprintf('%s: %s', _('IP'), htmlspecialchars($ip)), 'mod/view_ip.html', $args, $args['hostname']);
|
mod_page(sprintf('%s: %s', _('IP'), htmlspecialchars($ip)), 'mod/view_ip.html', $args, $args['hostname']);
|
||||||
}
|
}
|
||||||
|
|
1
mod.php
1
mod.php
|
@ -65,6 +65,7 @@ $pages = array(
|
||||||
'/reports/(\d+)/dismiss(all)?' => 'secure report_dismiss', // dismiss a report
|
'/reports/(\d+)/dismiss(all)?' => 'secure report_dismiss', // dismiss a report
|
||||||
|
|
||||||
'/IP/([\w.:]+)' => 'secure_POST ip', // view ip address
|
'/IP/([\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
|
'/IP/([\w.:]+)/remove_note/(\d+)' => 'secure ip_remove_note', // remove note from ip address
|
||||||
|
|
||||||
'/ban' => 'secure_POST ban', // new ban
|
'/ban' => 'secure_POST ban', // new ban
|
||||||
|
|
|
@ -213,3 +213,12 @@
|
||||||
{{ board_posts.posts|join('<hr>') }}
|
{{ board_posts.posts|join('<hr>') }}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
<div class="pages" style="margin-left: 50%">
|
||||||
|
<a href="?/IP/{{ ip }}">[Page 1]</a>
|
||||||
|
{% if cursor_prev %}
|
||||||
|
<a href="?/IP/{{ ip }}/cursor/{{ cursor_prev }}">[Previous Page]</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if cursor_next %}
|
||||||
|
<a href="?/IP/{{ ip }}/cursor/{{ cursor_next }}">[Next Page]</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue