forked from GithubBackups/vichan
Merge pull request #895 from Zankaria/user-posts
Browse users by password hash + refactor
This commit is contained in:
commit
3f467aacdf
15
inc/Data/PageFetchResult.php
Normal file
15
inc/Data/PageFetchResult.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
namespace Vichan\Data;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A page of user posts.
|
||||||
|
*/
|
||||||
|
class PageFetchResult {
|
||||||
|
/**
|
||||||
|
* @var array[array] Posts grouped by board uri.
|
||||||
|
*/
|
||||||
|
public array $by_uri;
|
||||||
|
public ?string $cursor_prev;
|
||||||
|
public ?string $cursor_next;
|
||||||
|
}
|
159
inc/Data/UserPostQueries.php
Normal file
159
inc/Data/UserPostQueries.php
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
<?php
|
||||||
|
namespace Vichan\Data;
|
||||||
|
|
||||||
|
use Vichan\Functions\Net;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Browse user posts
|
||||||
|
*/
|
||||||
|
class UserPostQueries {
|
||||||
|
private const CURSOR_TYPE_PREV = 'p';
|
||||||
|
private const CURSOR_TYPE_NEXT = 'n';
|
||||||
|
|
||||||
|
private \PDO $pdo;
|
||||||
|
|
||||||
|
public function __construct(\PDO $pdo) {
|
||||||
|
$this->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'");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1605,8 +1605,8 @@
|
|||||||
'default' => '/',
|
'default' => '/',
|
||||||
// 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).
|
||||||
'dns_lookup' => true,
|
'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 ?/user_posts/ip/x.x.x.x. and ?/user_posts/passwd/xxxxxxxx
|
||||||
'ip_recentposts' => 5,
|
'recent_user_posts' => 5,
|
||||||
// Number of posts to display on the reports page.
|
// Number of posts to display on the reports page.
|
||||||
'recent_reports' => 10,
|
'recent_reports' => 10,
|
||||||
// Number of actions to show per page in the moderation log.
|
// Number of actions to show per page in the moderation log.
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
namespace Vichan;
|
namespace Vichan;
|
||||||
|
|
||||||
use Vichan\Data\Driver\{CacheDriver, HttpDriver, ErrorLogLogDriver, FileLogDriver, LogDriver, StderrLogDriver, SyslogLogDriver};
|
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\HCaptchaQuery;
|
||||||
use Vichan\Service\SecureImageCaptchaQuery;
|
use Vichan\Service\SecureImageCaptchaQuery;
|
||||||
use Vichan\Service\ReCaptchaQuery;
|
use Vichan\Service\ReCaptchaQuery;
|
||||||
@ -108,5 +108,6 @@ function build_context(array $config): Context {
|
|||||||
return new ReportQueries($pdo, $auto_maintenance);
|
return new ReportQueries($pdo, $auto_maintenance);
|
||||||
},
|
},
|
||||||
IpNoteQueries::class => fn($c) => new IpNoteQueries($c->get(\PDO::class), $c->get(CacheDriver::class)),
|
IpNoteQueries::class => fn($c) => new IpNoteQueries($c->get(\PDO::class), $c->get(CacheDriver::class)),
|
||||||
|
UserPostQueries::class => fn($c) => new UserPostQueries($c->get(\PDO::class))
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -14,3 +14,63 @@ function is_connection_secure(bool $trust_headers): bool {
|
|||||||
}
|
}
|
||||||
return false;
|
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 ];
|
||||||
|
}
|
||||||
|
@ -3,10 +3,9 @@
|
|||||||
* Copyright (c) 2010-2013 Tinyboard Development Group
|
* Copyright (c) 2010-2013 Tinyboard Development Group
|
||||||
*/
|
*/
|
||||||
use Vichan\Context;
|
use Vichan\Context;
|
||||||
use Vichan\Data\{IpNoteQueries, ReportQueries};
|
use Vichan\Data\{IpNoteQueries, UserPostQueries, ReportQueries};
|
||||||
use Vichan\Functions\Format;
|
use Vichan\Functions\{Format, Net};
|
||||||
use Vichan\Functions\Net;
|
use Vichan\Data\Driver\{CacheDriver, LogDriver};
|
||||||
use Vichan\Data\Driver\CacheDriver;
|
|
||||||
|
|
||||||
defined('TINYBOARD') or exit;
|
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");
|
error("Note $id does not exist for $cloaked_ip");
|
||||||
}
|
}
|
||||||
|
|
||||||
modLog("Removed a note for <a href=\"?/IP/{$cloaked_ip}\">{$cloaked_ip}</a>");
|
modLog("Removed a note for <a href=\"?/user_posts/ip/{$cloaked_ip}\">{$cloaked_ip}</a>");
|
||||||
|
|
||||||
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) {
|
function mod_ip(Context $ctx, $cip) {
|
||||||
$ip = uncloak_ip($cip);
|
$ip = uncloak_ip($cip);
|
||||||
global $mod;
|
global $mod;
|
||||||
$config = $ctx->get('config');
|
$config = $ctx->get('config');
|
||||||
|
|
||||||
if (filter_var($ip, FILTER_VALIDATE_IP) === false)
|
if (filter_var($ip, FILTER_VALIDATE_IP) === false)
|
||||||
error("Invalid IP address.");
|
error('Invalid IP address.');
|
||||||
|
|
||||||
if (isset($_POST['ban_id'], $_POST['unban'])) {
|
if (isset($_POST['ban_id'], $_POST['unban'])) {
|
||||||
if (!hasPermission($config['mod']['unban']))
|
if (!hasPermission($config['mod']['unban']))
|
||||||
@ -918,7 +915,7 @@ function mod_ip(Context $ctx, $cip) {
|
|||||||
|
|
||||||
Bans::delete($_POST['ban_id'], true, $mod['boards']);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -940,45 +937,42 @@ function mod_ip(Context $ctx, $cip) {
|
|||||||
$note_queries = $ctx->get(IpNoteQueries::class);
|
$note_queries = $ctx->get(IpNoteQueries::class);
|
||||||
$note_queries->add($ip, $mod['id'], $_POST['note']);
|
$note_queries->add($ip, $mod['id'], $_POST['note']);
|
||||||
|
|
||||||
modLog("Added a note for <a href=\"?/IP/{$cip}\">{$cip}</a>");
|
modLog("Added a note for <a href=\"?/user_posts/ip/{$cip}\">{$cip}</a>");
|
||||||
|
|
||||||
header('Location: ?/IP/' . $cip . '#notes', true, $config['redirect_http']);
|
header("Location: ?/user_posts/ip/$cip#notes", true, $config['redirect_http']);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
\header("Location: ?/user_posts/ip/$cip", true, $config['redirect_http']);
|
||||||
|
}
|
||||||
|
|
||||||
$args = [];
|
function mod_user_posts_by_ip(Context $ctx, string $cip, ?string $encoded_cursor = null) {
|
||||||
$args['ip'] = $ip;
|
global $mod;
|
||||||
$args['posts'] = [];
|
|
||||||
|
|
||||||
if ($config['mod']['dns_lookup'] && empty($config['ipcrypt_key']))
|
$ip = uncloak_ip($cip);
|
||||||
$args['hostname'] = rDNS($ip);
|
|
||||||
|
|
||||||
$boards = listBoards();
|
if (\filter_var($ip, \FILTER_VALIDATE_IP) === false){
|
||||||
foreach ($boards as $board) {
|
error('Invalid IP address.');
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$args['boards'] = $boards;
|
$config = $ctx->get('config');
|
||||||
$args['token'] = make_secure_link_token('ban');
|
|
||||||
|
$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'])) {
|
if (hasPermission($config['mod']['view_ban'])) {
|
||||||
$args['bans'] = Bans::find($ip, false, true, null, $config['auto_maintenance']);
|
$args['bans'] = Bans::find($ip, false, true, null, $config['auto_maintenance']);
|
||||||
@ -990,19 +984,129 @@ function mod_ip(Context $ctx, $cip) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasPermission($config['mod']['modlog_ip'])) {
|
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");
|
$ret = Cache::get("mod_page_ip_modlog_ip_$ip");
|
||||||
$query->bindValue(':search', '%' . $cip . '%');
|
if (!$ret) {
|
||||||
$query->execute() or error(db_error($query));
|
$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");
|
||||||
$args['logs'] = $query->fetchAll(PDO::FETCH_ASSOC);
|
$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 {
|
} else {
|
||||||
$args['logs'] = [];
|
$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']);
|
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) {
|
function mod_edit_ban(Context $ctx, $ban_id) {
|
||||||
global $mod;
|
global $mod;
|
||||||
$config = $ctx->get('config');
|
$config = $ctx->get('config');
|
||||||
@ -1955,7 +2059,7 @@ function mod_deletebyip(Context $ctx, $boardName, $post, $global = false) {
|
|||||||
|
|
||||||
// Record the action
|
// Record the action
|
||||||
$cip = cloak_ip($ip);
|
$cip = cloak_ip($ip);
|
||||||
modLog("Deleted all posts by IP address: <a href=\"?/IP/$cip\">$cip</a>");
|
modLog("Deleted all posts by IP address: <a href=\"?/user_posts/ip/$cip\">$cip</a>");
|
||||||
|
|
||||||
// Redirect
|
// Redirect
|
||||||
header('Location: ?/' . sprintf($config['board_path'], $boardName) . $config['file_index'], true, $config['redirect_http']);
|
header('Location: ?/' . sprintf($config['board_path'], $boardName) . $config['file_index'], true, $config['redirect_http']);
|
||||||
@ -2542,7 +2646,7 @@ function mod_report_dismiss(Context $ctx, $id, $action) {
|
|||||||
|
|
||||||
$report_queries->deleteByIp($ip);
|
$report_queries->deleteByIp($ip);
|
||||||
$cip = cloak_ip($ip);
|
$cip = cloak_ip($ip);
|
||||||
modLog("Dismissed all reports by <a href=\"?/IP/$cip\">$cip</a>");
|
modLog("Dismissed all reports by <a href=\"?/user_posts/ip/$cip\">$cip</a>");
|
||||||
break;
|
break;
|
||||||
case '':
|
case '':
|
||||||
default:
|
default:
|
||||||
|
6
mod.php
6
mod.php
@ -99,6 +99,12 @@ class Router {
|
|||||||
'/IP/([\w.:]+)' => 'secure_POST ip', // view ip address
|
'/IP/([\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
|
||||||
|
|
||||||
|
'/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
|
'/ban' => 'secure_POST ban', // new ban
|
||||||
'/bans' => 'secure_POST bans', // ban list
|
'/bans' => 'secure_POST bans', // ban list
|
||||||
'/bans.json' => 'secure bans_json', // ban list JSON
|
'/bans.json' => 'secure bans_json', // ban list JSON
|
||||||
|
16
templates/mod/user_posts_list.html
Normal file
16
templates/mod/user_posts_list.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{% if posts|length == 0 %}
|
||||||
|
<p style="text-align:center">{% trans %}There are no posts.{% endtrans %}</p>
|
||||||
|
{% else %}
|
||||||
|
{% for board_posts in posts %}
|
||||||
|
<fieldset>
|
||||||
|
<legend>
|
||||||
|
<a href="?/{{ config.board_path|sprintf(board_posts.board.uri) }}{{ config.file_index }}">
|
||||||
|
{{ config.board_abbreviation|sprintf(board_posts.board.uri) }}
|
||||||
|
-
|
||||||
|
{{ board_posts.board.title|e }}
|
||||||
|
</a>
|
||||||
|
</legend>
|
||||||
|
{{ board_posts.posts|join('<hr>') }}
|
||||||
|
</fieldset>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
@ -1,24 +1,11 @@
|
|||||||
{% for board_posts in posts %}
|
{% if mod|hasPermission(config.mod.view_notes) and notes is not null %}
|
||||||
<fieldset>
|
|
||||||
<legend>
|
|
||||||
<a href="?/{{ config.board_path|sprintf(board_posts.board.uri) }}{{ config.file_index }}">
|
|
||||||
{{ config.board_abbreviation|sprintf(board_posts.board.uri) }}
|
|
||||||
-
|
|
||||||
{{ board_posts.board.title|e }}
|
|
||||||
</a>
|
|
||||||
</legend>
|
|
||||||
{{ board_posts.posts|join('<hr>') }}
|
|
||||||
</fieldset>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% if mod|hasPermission(config.mod.view_notes) %}
|
|
||||||
<fieldset id="notes">
|
<fieldset id="notes">
|
||||||
<legend>
|
<legend>
|
||||||
{% set notes_length = notes|length %}
|
{% set notes_length = notes|length %}
|
||||||
<legend>{{ notes_length }} {% trans %}note on record{% plural notes_length %}notes on record{% endtrans %}</legend>
|
<legend>{{ notes_length }} {% trans %}note on record{% plural notes_length %}notes on record{% endtrans %}</legend>
|
||||||
</legend>
|
</legend>
|
||||||
|
|
||||||
{% if notes|length > 0 %}
|
{% if notes and notes|length > 0 %}
|
||||||
<table class="modlog">
|
<table class="modlog">
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans 'Staff' %}</th>
|
<th>{% trans 'Staff' %}</th>
|
||||||
@ -56,7 +43,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if mod|hasPermission(config.mod.create_notes) %}
|
{% if mod|hasPermission(config.mod.create_notes) %}
|
||||||
<form action="" method="post" style="margin:0">
|
<form action="?/IP/{{ ip|url_encode(true) }}" method="post" style="margin:0">
|
||||||
<input type="hidden" name="token" value="{{ security_token }}">
|
<input type="hidden" name="token" value="{{ security_token }}">
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
@ -88,7 +75,7 @@
|
|||||||
<legend>{{ bans_length }} {% trans %}ban on record{% plural notes_length %}bans on record{% endtrans %}</legend>
|
<legend>{{ bans_length }} {% trans %}ban on record{% plural notes_length %}bans on record{% endtrans %}</legend>
|
||||||
|
|
||||||
{% for ban in bans %}
|
{% for ban in bans %}
|
||||||
<form action="" method="post" style="text-align:center">
|
<form action="?/IP/{{ ip|url_encode(true) }}" method="post" style="text-align:center">
|
||||||
<input type="hidden" name="token" value="{{ security_token }}">
|
<input type="hidden" name="token" value="{{ security_token }}">
|
||||||
{% include 'mod/ban_history.html' %}
|
{% include 'mod/ban_history.html' %}
|
||||||
<input type="hidden" name="ban_id" value="{{ ban.id }}">
|
<input type="hidden" name="ban_id" value="{{ ban.id }}">
|
||||||
@ -146,3 +133,14 @@
|
|||||||
</table>
|
</table>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{{ include('mod/user_posts_list.html', {posts: posts}) }}
|
||||||
|
<div class="pages" style="display:flex;justify-content:center;align-items:center;width:fit-content;margin:5px auto;">
|
||||||
|
<a href="?/user_posts/ip/{{ ip }}">[Page 1]</a>
|
||||||
|
{% if cursor_prev %}
|
||||||
|
<a href="?/user_posts/ip/{{ ip }}/cursor/{{ cursor_prev }}">[Previous Page]</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if cursor_next %}
|
||||||
|
<a href="?/user_posts/ip/{{ ip }}/cursor/{{ cursor_next }}">[Next Page]</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
10
templates/mod/view_passwd.html
Normal file
10
templates/mod/view_passwd.html
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{{ include('mod/user_posts_list.html', {posts: posts}) }}
|
||||||
|
<div class="pages" style="display:flex;justify-content:center;align-items:center;width:fit-content;margin:5px auto;">
|
||||||
|
<a href="?/user_posts/passwd/{{ passwd }}">[Page 1]</a>
|
||||||
|
{% if cursor_prev %}
|
||||||
|
<a href="?/user_posts/passwd/{{ passwd }}/cursor/{{ cursor_prev }}">[Previous Page]</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if cursor_next %}
|
||||||
|
<a href="?/user_posts/passwd/{{ passwd }}/cursor/{{ cursor_next }}">[Next Page]</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
@ -1,3 +1,4 @@
|
|||||||
{% if post.mod and post.mod|hasPermission(config.mod.show_ip, board.uri) %}
|
{% if post.mod and post.mod|hasPermission(config.mod.show_ip, board.uri) %}
|
||||||
<span>[<a class="ip-link" style="margin:0;" href="?/IP/{{ post.ip|cloak_ip }}">{{ post.ip|cloak_ip }}</a>]</span>
|
<span>[<a class="ip-link" style="margin:0;" href="?/user_posts/ip/{{ post.ip|cloak_ip }}">{{ post.ip|cloak_ip }}</a>]</span>
|
||||||
|
<span>[<a class="ip-link" style="margin:0;" href="?/user_posts/passwd/{{ post.password }}">{{ post.password[:15] }}</a>]</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user