diff --git a/inc/Data/ReportQueries.php b/inc/Data/ReportQueries.php
new file mode 100644
index 00000000..682ed917
--- /dev/null
+++ b/inc/Data/ReportQueries.php
@@ -0,0 +1,237 @@
+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();
+ }
+}
diff --git a/inc/context.php b/inc/context.php
index 3e4169cb..d1f74551 100644
--- a/inc/context.php
+++ b/inc/context.php
@@ -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);
}
]);
}
diff --git a/inc/mod/pages.php b/inc/mod/pages.php
index 8a549481..c51435fb 100644
--- a/inc/mod/pages.php
+++ b/inc/mod/pages.php
@@ -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, '
'));
@@ -2513,14 +2493,16 @@ function mod_reports(Context $ctx) {
$body .= $po->build(true) . '