1
0
mirror of https://github.com/vichan-devel/vichan.git synced 2025-02-22 13:30:02 +01:00

Merge pull request #850 from Zankaria/report-queries

Wrap report queries into a class
This commit is contained in:
Lorenzo Yario 2024-12-20 12:42:35 -06:00 committed by GitHub
commit 95e17a9506
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 302 additions and 64 deletions

237
inc/Data/ReportQueries.php Normal file
View File

@ -0,0 +1,237 @@
<?php
namespace Vichan\Data;
class ReportQueries {
private \PDO $pdo;
private bool $auto_maintenance;
private function deleteReportImpl(string $board, int $post_id) {
$query = $this->pdo->prepare('DELETE FROM `reports` WHERE `post` = :id AND `board` = :board');
$query->bindValue(':id', $post_id, \PDO::PARAM_INT);
$query->bindValue(':board', $board);
$query->execute();
}
private function joinReportPosts(array $raw_reports, ?int $limit): array {
// Group the reports rows by board.
$reports_by_boards = [];
foreach ($raw_reports as $report) {
if (!isset($reports_by_boards[$report['board']])) {
$reports_by_boards[$report['board']] = [];
}
$reports_by_boards[$report['board']][] = $report['post'];
}
// Join the reports with the actual posts.
$report_posts = [];
foreach ($reports_by_boards as $board => $posts) {
$report_posts[$board] = [];
$query = $this->pdo->prepare(\sprintf('SELECT * FROM `posts_%s` WHERE `id` IN (' . \implode(',', $posts) . ')', $board));
$query->execute();
while ($post = $query->fetch(\PDO::FETCH_ASSOC)) {
$report_posts[$board][$post['id']] = $post;
}
}
// Filter out the reports without a valid post.
$valid = [];
foreach ($raw_reports as $report) {
if (isset($report_posts[$report['board']][$report['post']])) {
$report['post_data'] = $report_posts[$report['board']][$report['post']];
$valid[] = $report;
if ($limit !== null && \count($valid) >= $limit) {
return $valid;
}
} else {
// Invalid report (post has been deleted).
if ($this->auto_maintenance != false) {
$this->deleteReportImpl($report['board'], $report['post']);
}
}
}
return $valid;
}
/**
* Filters out the invalid reports.
*
* @param array $raw_reports Array with the raw fetched reports. Must include a `board`, `post` and `id` fields.
* @param bool $get_invalid True to reverse the filter and get the invalid reports instead.
* @return array An array of filtered reports.
*/
private function filterReports(array $raw_reports, bool $get_invalid): array {
// Group the reports rows by board.
$reports_by_boards = [];
foreach ($raw_reports as $report) {
if (!isset($reports_by_boards[$report['board']])) {
$reports_by_boards[$report['board']] = [];
}
$reports_by_boards[$report['board']][] = $report['post'];
}
// Join the reports with the actual posts.
$report_posts = [];
foreach ($reports_by_boards as $board => $posts) {
$report_posts[$board] = [];
$query = $this->pdo->prepare(\sprintf('SELECT `id` FROM `posts_%s` WHERE `id` IN (' . \implode(',', $posts) . ')', $board));
$query->execute();
while ($post = $query->fetch(\PDO::FETCH_ASSOC)) {
$report_posts[$board][$post['id']] = $post;
}
}
if ($get_invalid) {
// Get the reports without a post.
$invalid = [];
foreach ($raw_reports as $report) {
if (isset($report_posts[$report['board']][$report['post']])) {
$invalid[] = $report;
}
}
return $invalid;
} else {
// Filter out the reports without a valid post.
$valid = [];
foreach ($raw_reports as $report) {
if (isset($report_posts[$report['board']][$report['post']])) {
$valid[] = $report;
} else {
// Invalid report (post has been deleted).
if ($this->auto_maintenance != false) {
$this->deleteReportImpl($report['board'], $report['post']);
}
}
}
return $valid;
}
}
/**
* @param \PDO $pdo PDO connection.
* @param bool $auto_maintenance If the auto maintenance should be enabled.
*/
public function __construct(\PDO $pdo, bool $auto_maintenance) {
$this->pdo = $pdo;
$this->auto_maintenance = $auto_maintenance;
}
/**
* Get the number of reports.
*
* @return int The number of reports.
*/
public function getCount(): int {
$query = $this->pdo->prepare('SELECT `board`, `post`, `id` FROM `reports`');
$query->execute();
$raw_reports = $query->fetchAll(\PDO::FETCH_ASSOC);
$valid_reports = $this->filterReports($raw_reports, false, null);
$count = \count($valid_reports);
return $count;
}
/**
* Get the report with the given id. DOES NOT PERFORM VALIDITY CHECK.
*
* @param int $id The id of the report to fetch.
* @return ?array An array of the given report with the `board`, `ip` and `post` fields. Null if no such report exists.
*/
public function getReportById(int $id): ?array {
$query = $this->pdo->prepare('SELECT `board`, `ip`, `post` FROM `reports` WHERE `id` = :id');
$query->bindValue(':id', $id);
$query->execute();
$ret = $query->fetch(\PDO::FETCH_ASSOC);
if ($ret !== false) {
return $ret;
} else {
return null;
}
}
/**
* Get the reports with the associated post data.
*
* @param int $count The maximum number of rows in the return array.
* @return array The reports with the associated post data.
*/
public function getReportsWithPosts(int $count): array {
$query = $this->pdo->prepare('SELECT * FROM `reports` ORDER BY `time`');
$query->execute();
$raw_reports = $query->fetchAll(\PDO::FETCH_ASSOC);
return $this->joinReportPosts($raw_reports, $count);
}
/**
* Purge the invalid reports.
*
* @return int The number of reports deleted.
*/
public function purge(): int {
$query = $this->pdo->prepare('SELECT `board`, `post`, `id` FROM `reports`');
$query->execute();
$raw_reports = $query->fetchAll(\PDO::FETCH_ASSOC);
$invalid_reports = $this->filterReports($raw_reports, true, null);
foreach ($invalid_reports as $report) {
$this->deleteReportImpl($report['board'], $report['post']);
}
return \count($invalid_reports);
}
/**
* Deletes the given report.
*
* @param int $id The report id.
*/
public function deleteById(int $id) {
$query = $this->pdo->prepare('DELETE FROM `reports` WHERE `id` = :id');
$query->bindValue(':id', $id, \PDO::PARAM_INT);
$query->execute();
}
/**
* Deletes all reports from the given ip.
*
* @param string $ip The reporter ip.
*/
public function deleteByIp(string $ip) {
$query = $this->pdo->prepare('DELETE FROM `reports` WHERE `ip` = :ip');
$query->bindValue(':ip', $ip);
$query->execute();
}
/**
* Deletes all reports from of the given post.
*
* @param int $post_id The post's id.
*/
public function deleteByPost(int $post_id) {
$query = $this->pdo->prepare('DELETE FROM `reports` WHERE `post` = :post');
$query->bindValue(':post', $post_id);
$query->execute();
}
/**
* Inserts a new report.
*
* @param string $ip Ip of the user sending the report.
* @param string $board_uri Board uri of the reported thread. MUST ALREADY BE SANITIZED.
* @param int $post_id Post reported.
* @param string $reason Reason of the report.
* @return void
*/
public function add(string $ip, string $board_uri, int $post_id, string $reason) {
$query = $this->pdo->prepare('INSERT INTO `reports` VALUES (NULL, :time, :ip, :board, :post, :reason)');
$query->bindValue(':time', time(), \PDO::PARAM_INT);
$query->bindValue(':ip', $ip);
$query->bindValue(':board', $board_uri);
$query->bindValue(':post', $post_id, \PDO::PARAM_INT);
$query->bindValue(':reason', $reason);
$query->execute();
}
}

View File

@ -2,6 +2,7 @@
namespace Vichan;
use Vichan\Data\Driver\{CacheDriver, HttpDriver, ErrorLogLogDriver, FileLogDriver, LogDriver, StderrLogDriver, SyslogLogDriver};
use Vichan\Data\ReportQueries;
use Vichan\Service\HCaptchaQuery;
use Vichan\Service\SecureImageCaptchaQuery;
use Vichan\Service\ReCaptchaQuery;
@ -92,6 +93,17 @@ function build_context(array $config): Context {
CacheDriver::class => function($c) {
// Use the global for backwards compatibility.
return \cache::getCache();
},
\PDO::class => function($c) {
global $pdo;
// Ensure the PDO is initialized.
sql_open();
return $pdo;
},
ReportQueries::class => function($c) {
$auto_maintenance = (bool)$c->get('config')['auto_maintenance'];
$pdo = $c->get(\PDO::class);
return new ReportQueries($pdo, $auto_maintenance);
}
]);
}

View File

@ -3,6 +3,7 @@
* Copyright (c) 2010-2013 Tinyboard Development Group
*/
use Vichan\Context;
use Vichan\Data\ReportQueries;
use Vichan\Functions\Format;
use Vichan\Functions\Net;
use Vichan\Data\Driver\CacheDriver;
@ -106,6 +107,7 @@ function mod_logout(Context $ctx) {
function mod_dashboard(Context $ctx) {
global $mod;
$config = $ctx->get('config');
$report_queries = $ctx->get(ReportQueries::class);
$args = [];
@ -131,8 +133,7 @@ function mod_dashboard(Context $ctx) {
$ctx->get(CacheDriver::class)->set('pm_unreadcount_' . $mod['id'], $args['unread_pms']);
}
$query = query('SELECT COUNT(*) FROM ``reports``') or error(db_error($query));
$args['reports'] = $query->fetchColumn();
$args['reports'] = $report_queries->getCount();
$query = query('SELECT COUNT(*) FROM ``ban_appeals``') or error(db_error($query));
$args['appeals'] = $query->fetchColumn();
@ -2444,43 +2445,22 @@ function mod_reports(Context $ctx) {
if (!hasPermission($config['mod']['reports']))
error($config['error']['noaccess']);
$query = prepare("SELECT * FROM ``reports`` ORDER BY `time` DESC LIMIT :limit");
$query->bindValue(':limit', $config['mod']['recent_reports'], PDO::PARAM_INT);
$query->execute() or error(db_error($query));
$reports = $query->fetchAll(PDO::FETCH_ASSOC);
$reports_limit = $config['mod']['recent_reports'];
$report_queries = $ctx->get(ReportQueries::class);
$report_rows = $report_queries->getReportsWithPosts($reports_limit);
$report_queries = [];
foreach ($reports as $report) {
if (!isset($report_queries[$report['board']]))
$report_queries[$report['board']] = [];
$report_queries[$report['board']][] = $report['post'];
if (\count($report_rows) > $reports_limit) {
\array_pop($report_rows);
$has_extra = true;
} else {
$has_extra = false;
}
$report_posts = [];
foreach ($report_queries as $board => $posts) {
$report_posts[$board] = [];
$query = query(sprintf('SELECT * FROM ``posts_%s`` WHERE `id` = ' . implode(' OR `id` = ', $posts), $board)) or error(db_error());
while ($post = $query->fetch(PDO::FETCH_ASSOC)) {
$report_posts[$board][$post['id']] = $post;
}
}
$count = 0;
$body = '';
foreach ($reports as $report) {
if (!isset($report_posts[$report['board']][$report['post']])) {
// // Invalid report (post has since been deleted)
$query = prepare("DELETE FROM ``reports`` WHERE `post` = :id AND `board` = :board");
$query->bindValue(':id', $report['post'], PDO::PARAM_INT);
$query->bindValue(':board', $report['board']);
$query->execute() or error(db_error($query));
continue;
}
foreach ($report_rows as $report) {
openBoard($report['board']);
$post = &$report_posts[$report['board']][$report['post']];
$post = $report['post_data'];
if (!$post['thread']) {
// Still need to fix this:
@ -2489,16 +2469,16 @@ function mod_reports(Context $ctx) {
$po = new Post($post, '?/', $mod);
}
// a little messy and inefficient
$append_html = Element($config['file_mod_report'], array(
// A little messy and inefficient.
$append_html = Element($config['file_mod_report'], [
'report' => $report,
'config' => $config,
'mod' => $mod,
'pm' => create_pm_header(),
'token' => make_secure_link_token('reports/' . $report['id'] . '/dismiss'),
'token_all' => make_secure_link_token('reports/' . $report['id'] . '/dismiss&all'),
'token_post' => make_secure_link_token('reports/'. $report['id'] . '/dismiss&post'),
));
'token_post' => make_secure_link_token('reports/'. $report['id'] . '/dismiss&post')
]);
// Bug fix for https://github.com/savetheinternet/Tinyboard/issues/21
$po->body = truncate($po->body, $po->link(), $config['body_truncate'] - substr_count($append_html, '<br>'));
@ -2513,14 +2493,16 @@ function mod_reports(Context $ctx) {
$body .= $po->build(true) . '<hr>';
if (isset($__old_body_truncate_char))
if (isset($__old_body_truncate_char)) {
$config['body_truncate_char'] = $__old_body_truncate_char;
$count++;
}
}
$count = \count($report_rows);
$header_count = $has_extra ? "{$count}+" : (string)$count;
mod_page(
sprintf('%s (%d)', _('Report queue'), $count),
sprintf('%s (%s)', _('Report queue'), $header_count),
$config['file_mod_reports'],
[
'reports' => $body,
@ -2533,31 +2515,29 @@ function mod_reports(Context $ctx) {
function mod_report_dismiss(Context $ctx, $id, $action) {
$config = $ctx->get('config');
$query = prepare("SELECT `post`, `board`, `ip` FROM ``reports`` WHERE `id` = :id");
$query->bindValue(':id', $id);
$query->execute() or error(db_error($query));
if ($report = $query->fetch(PDO::FETCH_ASSOC)) {
$ip = $report['ip'];
$board = $report['board'];
$post = $report['post'];
} else
$report_queries = $ctx->get(ReportQueries::class);
$report = $report_queries->getReportById($id);
if ($report === null) {
error($config['error']['404']);
}
$ip = $report['ip'];
$board = $report['board'];
$post = $report['post'];
switch($action){
case '&post':
if (!hasPermission($config['mod']['report_dismiss_post'], $board))
error($config['error']['noaccess']);
$query = prepare("DELETE FROM ``reports`` WHERE `post` = :post");
$query->bindValue(':post', $post);
$report_queries->deleteByPost($post);
modLog("Dismissed all reports for post #{$id}", $board);
break;
case '&all':
if (!hasPermission($config['mod']['report_dismiss_ip'], $board))
error($config['error']['noaccess']);
$query = prepare("DELETE FROM ``reports`` WHERE `ip` = :ip");
$query->bindValue(':ip', $ip);
$report_queries->deleteByIp($ip);
$cip = cloak_ip($ip);
modLog("Dismissed all reports by <a href=\"?/IP/$cip\">$cip</a>");
break;
@ -2566,12 +2546,10 @@ function mod_report_dismiss(Context $ctx, $id, $action) {
if (!hasPermission($config['mod']['report_dismiss'], $board))
error($config['error']['noaccess']);
$query = prepare("DELETE FROM ``reports`` WHERE `id` = :id");
$query->bindValue(':id', $id);
$report_queries->deleteById($id);
modLog("Dismissed a report for post #{$id}", $board);
break;
}
$query->execute() or error(db_error($query));
header('Location: ?/reports', true, $config['redirect_http']);
}

View File

@ -7,6 +7,7 @@ require_once 'inc/bootstrap.php';
use Vichan\{Context, WebDependencyFactory};
use Vichan\Data\Driver\{LogDriver, HttpDriver};
use Vichan\Data\ReportQueries;
use Vichan\Service\{RemoteCaptchaQuery, SecureImageCaptchaQuery};
use Vichan\Functions\{Format, IP};
@ -543,6 +544,8 @@ if (isset($_POST['delete'])) {
error($config['error']['toolongreport']);
}
$report_queries = $context->get(ReportQueries::class);
foreach ($report as &$id) {
$query = prepare(sprintf("SELECT `id`, `thread` FROM ``posts_%s`` WHERE `id` = :id", $board['uri']));
$query->bindValue(':id', $id, PDO::PARAM_INT);
@ -565,13 +568,8 @@ if (isset($_POST['delete'])) {
. $board['dir'] . $config['dir']['res'] . link_for($post) . ($post['thread'] ? '#' . $id : '')
. " for \"$reason\""
);
$query = prepare("INSERT INTO ``reports`` VALUES (NULL, :time, :ip, :board, :post, :reason)");
$query->bindValue(':time', time(), PDO::PARAM_INT);
$query->bindValue(':ip', $_SERVER['REMOTE_ADDR'], PDO::PARAM_STR);
$query->bindValue(':board', $board['uri'], PDO::PARAM_STR);
$query->bindValue(':post', $id, PDO::PARAM_INT);
$query->bindValue(':reason', $reason, PDO::PARAM_STR);
$query->execute() or error(db_error($query));
$report_queries->add($_SERVER['REMOTE_ADDR'], $board['uri'], $id, $reason);
}
$is_mod = isset($_POST['mod']) && $_POST['mod'];

View File

@ -3,8 +3,12 @@
* Performs maintenance tasks. Invoke this periodically if the auto_maintenance configuration option is turned off.
*/
use Vichan\Data\ReportQueries;
require dirname(__FILE__) . '/inc/cli.php';
$ctx = Vichan\build_context($config);
echo "Clearing expired bans...\n";
$start = microtime(true);
$deleted_count = Bans::purge($config['require_ban_view'], $config['purge_bans']);
@ -19,7 +23,16 @@ $deleted_count = purge_old_antispam();
$delta = microtime(true) - $start;
echo "Deleted $deleted_count expired antispam in $delta seconds!\n";
$time_tot = $delta;
$deleted_tot = $deleted_count;
$deleted_tot += $deleted_count;
echo "Clearing invalid reports...\n";
$report_queries = $ctx->get(ReportQueries::class);
$start = microtime(true);
$deleted_count = $report_queries->purge();
$delta = microtime(true) - $start;
echo "Deleted $deleted_count invalid reports in $delta seconds!\n";
$time_tot += $delta;
$deleted_tot += $deleted_count;
if ($config['cache']['enabled'] === 'fs') {
$fs_cache = new Vichan\Data\Driver\FsCacheDriver(