4chan/reports/ReportQueue-test.php
2025-04-17 18:12:08 -05:00

2915 lines
72 KiB
PHP

<?php
require_once 'lib/geoip2.php';
class ReportQueue {
protected
$actions = array(
'reports',
'ban_requests',
'get_reports',
'clear_report',
'get_reporters',
'get_rep_details',
'ban_reporters',
'clear_reporter',
'get_ban_requests',
'accept_ban_request',
'deny_ban_request',
'get_ip',
'count_reports',
'get_report_ids',
'get_templates',
'clear_stale_reports',
'staffmessages'
),
$isCountReports,
$access;
protected $report_abuse_tpl = null;
protected $cached_board_coefs = null;
const DATE_FORMAT ='m/d/y H:i';
const REP_ABUSE_TPL = 190;
const PAGE_SIZE = 25;
const GLOBAL_THRES = 1500; // Weight after which the report is globally unlocked to all janitors
const HIGHLIGHT_THRES = 500;
const THREAD_WEIGHT_BOOST = 1.25; // weight multiplier for threads, rounded up
const STATS_PRUNE_MONTHS = 6; // limit in month for pruning janitor stats
const STATS_ACTION_DENIED = 0;
const STATS_ACTION_ACCEPTED = 1;
const MAX_REPORTS_PER_PAGE = 3000;
// after X local bans on different boards, a local ban will be turned into a global one
const LOCAL_TO_GLOBAL_THRES = 1;
// time interval in days to consider when deciding to upgrade report abuse warnings to bans
const ABUSE_WARN_UPGRADE_INTERVAL = 30;
// when to prune the cleared log, in days
const CLEAR_LOG_AGE = 5;
// Automatic warnings/bans for abuse based on the number of cleared reports
const ABUSE_CLEAR_DAYS = 3;
const ABUSE_CLEAR_COUNT = 50;
const ABUSE_CLEAR_BAN_INTERVAL = 5; // Minimum number of days between auto-bans for abuse
const WS_ANY = 0;
const WS_ONLY = 1;
const WS_NOT = 2;
static public function denied() {
require_once('views/denied.tpl.php');
die();
}
function __construct($my_access, $isCountReports = false) {
if (!$my_access || !is_array($my_access)) {
ReportQueue::denied();
}
$this->access = $my_access;
$this->isCountReports = $isCountReports;
$this->isMod = $this->access['ban'] === 1;
$this->isAdmin = isset($this->access['is_admin']) && $this->access['is_admin'];
$this->isManager = isset($this->access['is_manager']) && $this->access['is_manager'];
$this->isDeveloper = isset($this->access['is_developer']) && $this->access['is_developer'];
/*
if ($this->isDeveloper) {
$mysql_suppress_err = false;
ini_set('display_errors', 1);
error_reporting(E_ALL);
}
*/
if ($this->isAdmin) {
$this->userLevel = 'admin';
}
else if ($this->isManager) {
$this->userLevel = 'manager';
}
else if ($this->isMod) {
$this->userLevel = 'mod';
}
else {
$this->userLevel = 'janitor';
}
}
private function success($data = null, $add_length = false) {
$this->renderJSON(array('status' => 'success', 'data' => $data), $add_length);
}
private function fail($data = null) {
$this->renderJSON(array('status' => 'fail', 'data' => $data));
die();
}
private function error($message, $code = null, $data = null) {
$payload = array('status' => 'error', 'message' => $message);
if ($code) {
$payload['code'] = $code;
}
if ($data) {
$payload['data'] = $data;
}
$this->renderJSON($payload);
die();
}
private function renderJSON($data, $add_length = false) {
header('Content-Type: application/json');
$data = json_encode($data, JSON_PARTIAL_OUTPUT_ON_ERROR);
echo $data;
}
private function renderHTML($view) {
include('views/' . $view . '.tpl.php');
}
private function validate_csrf($value='') {
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if (!isset($_COOKIE['_tkn']) || !isset($_POST['_tkn'])
|| $_COOKIE['_tkn'] == '' || $_POST['_tkn'] == ''
|| $_COOKIE['_tkn'] !== $_POST['_tkn']) {
$this->error('Bad Request');
}
}
else {
if (isset($_SERVER['HTTP_REFERER']) && $_SERVER['HTTP_REFERER'] != ''
&& !preg_match('/^https?:\/\/([_a-z0-9]+)\.(4chan|4channel)\.org(\/|$)/', $_SERVER['HTTP_REFERER'])) {
$this->error('Bad Request');
}
}
}
private function prune_clear_log() {
$days = (int)self::CLEAR_LOG_AGE;
$sql = "DELETE FROM report_clear_log WHERE created_on < DATE_SUB(NOW(), INTERVAL $days DAY)";
return mysql_global_call($sql);
}
private function write_to_event_log($event, $ip, $args = []) {
$sql = <<<SQL
INSERT INTO event_log(`type`, ip, board, thread_id, post_id, arg_num,
arg_str, pwd, req_sig, ua_sig, meta)
VALUES('%s', '%s', '%s', '%d', '%d', '%d',
'%s', '%s', '%s', '%s', '%s')
SQL;
return mysql_global_call($sql, $event, $ip,
$args['board'], $args['thread_id'], $args['post_id'], $args['arg_num'],
$args['arg_str'], $args['pwd'], $args['req_sig'], $args['ua_sig'], $args['meta']
);
}
private function get_report_abuse_template() {
if ($this->report_abuse_tpl) {
return $this->report_abuse_tpl;
}
$rep_abuse_tpl = (int)self::REP_ABUSE_TPL;
$result = mysql_global_call("SELECT * FROM ban_templates WHERE no = $rep_abuse_tpl");
$template = mysql_fetch_assoc($result);
if (!$template) {
return null;
}
$this->report_abuse_tpl = $template;
return $template;
}
// Logs cleared reports to a separate table
private function log_cleared_reporter($long_ip, $pwd, $pass_id, $cat_id, $weight) {
$sql = <<<SQL
INSERT INTO report_clear_log(long_ip, pwd, pass_id, category, weight)
VALUES(%d, '%s', '%s', %d, %F)
SQL;
return !!mysql_global_call($sql, $long_ip, $pwd, $pass_id, $cat_id, $weight);
}
// Checks for abuse and issues warnings/bans
private function enforce_cleared_abuse($long_ip, $pwd, $pass_id) {
$cleared_days_lim = (int)self::ABUSE_CLEAR_DAYS;
$cleared_count_lim = (int)self::ABUSE_CLEAR_COUNT;
$ban_days_lim = (int)self::ABUSE_CLEAR_BAN_INTERVAL;
$rep_abuse_tpl = (int)self::REP_ABUSE_TPL;
$ip = long2ip($long_ip);
if (!$ip) {
return false;
}
// Double ban query because indexes all cover 'active'
// 4chan Pass
if ($pass_id) {
$pass_id_sql = mysql_real_escape_string($pass_id);
$ban_clauses[] = "active = 0 AND 4pass_id = '$pass_id_sql'";
$ban_clauses[] = "active = 1 AND 4pass_id = '$pass_id_sql'";
$rep_clauses[] = "pass_id = '$pass_id_sql'";
}
// IP
else {
$ban_clauses[] = "host = '" . mysql_real_escape_string($ip) . "'";
$rep_clauses[] = "long_ip = " . (int)$long_ip;
}
// Password
if ($pwd) {
$pwd_sql = mysql_real_escape_string($pwd);
$ban_clauses[] = "active = 0 AND password = '$pwd_sql'";
$ban_clauses[] = "active = 1 AND password = '$pwd_sql'";
$rep_clauses[] = "pwd = '$pwd_sql'";
}
// Check cleared reports
$clause = implode(' OR ', $rep_clauses);
$query = <<<SQL
SELECT SQL_NO_CACHE COUNT(*) FROM report_clear_log
WHERE created_on > DATE_SUB(NOW(), INTERVAL $cleared_days_lim DAY)
AND ($clause)
SQL;
$res = mysql_global_call($query);
if (!$res) {
return false;
}
$clear_count = (int)mysql_fetch_row($res)[0];
if ($clear_count < $cleared_count_lim) {
return false;
}
// Cleared threshold reached, check if a warn/ban is needed
foreach ($ban_clauses as $clause) {
$query = <<<SQL
SELECT 1 FROM banned_users
WHERE $clause AND template_id = $rep_abuse_tpl
AND now > DATE_SUB(NOW(), INTERVAL $ban_days_lim DAY)
SQL;
$res = mysql_global_call($query);
if (!$res) {
return false;
}
if (mysql_num_rows($res) > 0) {
return false;
}
}
// Issue warning
$template = $this->get_report_abuse_template();
if (!$template) {
return false;
}
//$this->write_to_event_log('clear_lim_warn', $ip);
$reason = $template['publicreason'] . '<>Clear limit reached';
$query =<<<SQL
INSERT INTO banned_users (
board, global, zonly, name, host, reverse, xff, reason, length,
admin, md5, post_num, rule, post_time, template_id, password, 4pass_id,
post_json, admin_ip
)
VALUES ('', %d, %d, 'Anonymous', '%s', '%s', '', '%s', '%s',
'%s', '', 0, '%s', '', %d, '%s', '%s',
'', '')
SQL;
$length = date('Y-m-d H:i:s', time());
$result = mysql_global_call($query,
1, 0, $ip, $ip, $reason, $length,
'Auto-ban', $template['rule'], $rep_abuse_tpl, $pwd, $pass_id
);
if (!$result) {
return false;
}
return true;
}
// parameters should already be validated and escaped for SQL
private function is_report_for_own_post($board, $no) {
$sql = "SELECT host FROM `$board` WHERE no = $no LIMIT 1";
$res = mysql_board_call($sql);
if (!$res) {
return false;
}
$ip = mysql_fetch_row($res);
if (!$ip) {
return false;
}
return $ip[0] === $_SERVER['REMOTE_ADDR'];
}
private function get_request_ids() {
$query = "SELECT id FROM ban_requests";
$res = mysql_global_call($query);
if (!$res) {
return null;
}
$data = array();
while ($row = mysql_fetch_row($res)) {
$data[] = (int)$row[0];
}
return $data;
}
private function log_self_clear($board, $post_id, $post_json) {
$sql = <<<SQL
INSERT INTO event_log(`type`, ip, board, post_id, arg_str, pwd, meta)
VALUES('staff_self_clear', '%s', '%s', '%d', '%s', '%s', '%s')
SQL;
return mysql_global_call($sql, $_SERVER['REMOTE_ADDR'], $board, $post_id,
$_COOKIE['4chan_auser'], $_COOKIE['4chan_pass'], $post_json
);
}
private function update_janitor_stats($username, $action, $board, $post_id, $requested_tpl, $accepted_tpl, $mod_username) {
$prune_lim = (int)self::STATS_PRUNE_MONTHS;
$query = "SELECT id FROM mod_users WHERE username = '%s' LIMIT 1";
$res = mysql_global_call($query, $username);
if (!$res) {
return false;
}
$user_id = (int)mysql_fetch_row($res)[0];
if (!$user_id) {
return false;
}
$query = "SELECT id FROM mod_users WHERE username = '%s' LIMIT 1";
$res = mysql_global_call($query, $mod_username);
if (!$res) {
return false;
}
$mod_user_id = (int)mysql_fetch_row($res)[0];
if (!$mod_user_id) {
return false;
}
$query = <<<SQL
INSERT INTO janitor_stats(user_id, action_type, board, post_id, requested_tpl, accepted_tpl, created_by_id)
VALUES(%d, %d, '%s', %d, %d, %d, %d)
SQL;
$res = mysql_global_call($query, $user_id, $action, $board, $post_id, $requested_tpl, $accepted_tpl, $mod_user_id);
if (!$res) {
return false;
}
$query = "DELETE FROM janitor_stats WHERE created_on < DATE_SUB(NOW(), INTERVAL $prune_lim MONTH)";
mysql_global_call($query);
return true;
}
// Counts ban requests for each board
private function count_board_brs() {
$query = "SELECT COUNT(id) as cnt, board FROM ban_requests GROUP BY board";
$res = mysql_global_call($query);
if (!$res) {
return array();
}
$data = array();
while ($row = mysql_fetch_row($res)) {
$data[$row[1]] = $row[0];
}
return $data;
}
// Counts reports for each board
private function count_board_reports() {
$data = array();
if (isset($this->access['board'])) {
$board_clause = array();
foreach($this->access['board'] as $b) {
if (!preg_match('/^[a-z0-9]+$/', $b)) {
return $data;
}
$board_clause[] = $b;
}
if (empty($board_clause)) {
$board_clause = '';
}
else {
$board_clause = "AND board IN('" . implode("','", $board_clause) . "')";
}
}
else {
$board_clause = '';
}
$query = <<<SQL
SELECT SUM(weight) as total_weight, board, no, resto
FROM `reports`
WHERE cleared = 0 $board_clause
GROUP BY board, no
SQL;
$res = mysql_global_call($query);
if (!$res) {
return $data;
}
$board_coefs = $this->get_board_coefficients();
while ($report = mysql_fetch_assoc($res)) {
$total_weight = (float)$report['total_weight'];
if (isset($board_coefs[$report['board']])) {
$total_weight = $total_weight * $board_coefs[$report['board']];
}
if (!$report['resto']) {
$total_weight = $total_weight * self::THREAD_WEIGHT_BOOST;
}
if ($total_weight < 1.00) {
continue;
}
if (!isset($data[$report['board']])) {
$data[$report['board']] = 0;
}
$data[$report['board']]++;
}
return $data;
}
private function is_board_valid($board) {
if ($board === 'test') {
return true;
}
$query = "SELECT dir FROM boardlist WHERE dir = '%s' LIMIT 1";
$result = mysql_global_call($query, $board);
return $result && mysql_num_rows($result) === 1;
}
private function get_bans_summary($ip, $pass = null) {
$base_query = <<<SQL
SELECT SQL_NO_CACHE UNIX_TIMESTAMP(`now`) as created_on,
UNIX_TIMESTAMP(`length`) as expires_on,
UNIX_TIMESTAMP(`unbannedon`) as unbanned_on,
template_id
FROM banned_users
SQL;
$query = $base_query . " WHERE host = '%s'";
$res = mysql_global_call($query, $ip);
if (!$res) {
return array();
}
$bans = array();
$template_ids = [];
while ($row = mysql_fetch_assoc($res)) {
$bans[] = $row;
}
if ($pass) {
$query = $base_query . " WHERE (active = 0 OR active = 1) AND 4pass_id = '%s' AND host != '%s'";
$res = mysql_global_call($query, $pass, $ip);
if ($res) {
while ($row = mysql_fetch_assoc($res)) {
$bans[] = $row;
}
}
}
$now = $_SERVER['REQUEST_TIME'];
$limit = $now - 31536000; // 1 year
$warn_limit_low = $now - 86400; // 1 day
$warn_limit_high = $now - 3600; // 1 hour
$total_count = count($bans);
$active_bans = [];
$active_warns = [];
$recent_ban_count = 0;
$recent_warn_count = 0;
$recent_duration = 0; // in days
foreach ($bans as $ban) {
if (!$ban['expires_on']) {
continue;
}
if ($ban['created_on'] < $limit) {
continue;
}
$ban_len = $ban['expires_on'] - $ban['created_on'];
if ($ban_len <= 10) {
++$recent_warn_count;
if ($ban['created_on'] >= $warn_limit_low && $ban['created_on'] <= $warn_limit_high) {
$active_warns[] = $ban['template_id'];
}
}
else {
if ($ban['unbanned_on']) {
$spent_len = $ban['unbanned_on'] - $ban['created_on'];
if ($spent_len > $ban_len) {
$spent_len = $ban_len;
}
}
else if ($ban['expires_on'] > $now) {
$spent_len = $now - $ban['created_on'];
$active_bans[] = $ban['template_id'];
}
else {
$spent_len = $ban_len;
}
$recent_duration += $spent_len;
++$recent_ban_count;
}
}
if ($recent_duration) {
$recent_duration = ceil($recent_duration / 86400.0);
}
$ret = array(
'total' => $total_count,
'recent_bans' => $recent_ban_count,
'recent_warns' => $recent_warn_count,
'recent_days' => $recent_duration,
'active_bans' => $active_bans,
'active_warns' => $active_warns
);
return $ret;
}
private function format_staff_message($msg) {
return preg_replace('@https://i.4cdn.org/j/[a-z0-9]+/[0-9]+\.(?:jpg|png|gif)@', '<img alt="" src="\\0">', $msg);
}
private function get_abuse_bans_summary($ip, $pass_id = null) {
$interval_min = 86400; // 1 day
$interval_max = self::ABUSE_WARN_UPGRADE_INTERVAL * 86400;
$abuse_tpl = (int)self::REP_ABUSE_TPL;
$base_query = <<<SQL
SELECT SQL_NO_CACHE UNIX_TIMESTAMP(`now`) as created_on,
UNIX_TIMESTAMP(`length`) as expires_on
FROM banned_users
WHERE active = 0 AND template_id = $abuse_tpl
SQL;
$warn_count = 0;
$ban_count = 0;
if ($pass_id) {
$query = $base_query . " AND 4pass_id = '%s'";
$res = mysql_global_call($query, $pass_id);
}
else {
$query = $base_query . " AND host = '%s'";
$res = mysql_global_call($query, $ip);
}
if ($res) {
$now = $_SERVER['REQUEST_TIME'];
$limit_min = $now - $interval_min;
$limit_max = $now - $interval_max;
while ($ban = mysql_fetch_assoc($res)) {
if ($ban['created_on'] < $limit_max || $ban['created_on'] > $limit_min) {
continue;
}
if ($ban['expires_on'] && ($ban['expires_on'] - $ban['created_on'] <= 10)) {
$warn_count++;
}
else {
$ban_count++;
}
}
}
return [$warn_count, $ban_count];
}
private function get_board_list() {
$query = "SELECT dir FROM boardlist";
$result = mysql_global_call($query);
$boards = array();
if (mysql_num_rows($result) > 0) {
while ($board = mysql_fetch_array($result)) {
$boards[] = $board[0];
}
}
return $boards;
}
// weight multipliers for boards
private function get_board_coefficients() {
if ($this->cached_board_coefs !== null) {
return $this->cached_board_coefs;
}
$query = "SELECT boards, coef FROM report_settings";
$res = mysql_global_call($query);
$data = array();
if (!$res) {
return $data;
}
while ($row = mysql_fetch_assoc($res)) {
$boards = explode(',', $row['boards']);
foreach ($boards as $b) {
$data[$b] = (float)$row['coef'];
}
}
$this->cached_board_coefs = $data;
return $data;
}
private function can_access_board($board) {
if (isset($this->access['board'])) {
return in_array($board, $this->access['board']);
}
return true;
}
private function is_report_unlocked($board, $pid) {
// Arguments should already be escaped for SQL
$weight_boost = self::THREAD_WEIGHT_BOOST;
$query = <<<SQL
SELECT CEIL(SUM(IF(resto > 0, weight, weight * $weight_boost))) as total_weight
FROM reports
WHERE board = '$board' AND no = $pid
SQL;
$result = mysql_global_call($query);
if (!$result) {
return false;
}
$total_weight = (int)mysql_fetch_row($result)[0];
if (!$total_weight) {
return false;
}
return $total_weight >= self::GLOBAL_THRES;
}
private function delete_by_ip($board, $ip) {
include_once 'lib/rpc.php';
$url = 'admin.php';
$data = array(
'admin' => 'delallbyip',
'ip' => $ip
);
rpc_start_request("https://sys.int/$board/admin.php", $data, $_COOKIE, true);
}
private function fetch_reports($board, $cleared_only = false, $ws_mode = self::WS_ANY) {
/* board */
$board_clause = null;
if (isset($this->access['board'])) {
if ($board === false) {
$board_clause = array();
foreach($this->access['board'] as $b) {
$board_clause[] = "reports.board = '" . mysql_real_escape_string($b) . "'";
}
$board_clause = '(' . implode(' OR ', $board_clause) . ')';
}
elseif (!in_array($board, $this->access['board'])) {
$this->error("You can't view reports for this board.");
}
}
$cleared_only = (int)$cleared_only;
$where = "WHERE cleared = $cleared_only";
$prio = 'total_weight >= ' . self::GLOBAL_THRES;
if ($board) {
$board = mysql_real_escape_string($board);
$having = "HAVING (reports.board = '$board' OR $prio)";
}
else if ($ws_mode === self::WS_ONLY) {
$having = "HAVING (reports.ws = 1 OR $prio)";
}
else if ($ws_mode === self::WS_NOT) {
$having = "HAVING (reports.ws = 0 OR $prio)";
}
else if ($board_clause) {
$having = "HAVING ($board_clause OR $prio)";
}
$weight_boost = self::THREAD_WEIGHT_BOOST;
$query = <<<SQL
SELECT *, SUM(weight) as total_weight, COUNT(reports.no) as cnt,
GROUP_CONCAT(DISTINCT report_category) as cats,
UNIX_TIMESTAMP(ts) as `time`
FROM reports
$where
GROUP BY reports.no, reports.board
$having
SQL;
return mysql_global_call($query);
}
private function fetch_report_counts() {
$board_clause = null;
if (isset($this->access['board'])) {
$board_clause = array();
foreach($this->access['board'] as $b) {
$board_clause[] = "reports.board = '" . mysql_real_escape_string($b) . "'";
}
$board_clause = '(' . implode(' OR ', $board_clause) . ')';
}
$having = "HAVING total_weight >= 0";
if ($board_clause) {
$prio = 'total_weight >= ' . self::GLOBAL_THRES;
$having .= " AND ($board_clause OR $prio)";
}
$query = <<<SQL
SELECT SUM(weight) as total_weight, resto, reports.board
FROM reports
WHERE cleared = 0
GROUP BY reports.no, reports.board
$having
SQL;
return mysql_global_call($query);
}
private function build_report($report) {
$data = array(
'id' => (int)$report['id'],
'board' => $report['board'],
'no' => (int)$report['no'],
'count' => (int)$report['cnt'],
'weight' => (float)$report['total_weight'],
'ts' => (int)$report['time'],
'post' => $report['post_json']
);
return $data;
}
private function clear_ban_request($id, $salt, $has_approved = false) {
$id = (int)$id;
$result = mysql_global_call("SELECT * FROM ban_requests WHERE id = $id");
if (!mysql_num_rows($result)) {
return false;
}
$request = mysql_fetch_array($result);
$result = mysql_global_call("DELETE FROM ban_requests WHERE id = $id");
if (!$result) {
$this->error('Database error (cbr0)');
}
$post = json_decode($request['post_json']);
$hash = sha1($request['board'] . $post->no . $salt);
$ban_thumb_dir = '/www/4chan.org/web/images/bans/thumb/';
$ban_image_dir = '/www/4chan.org/web/images/bans/src/';
@unlink($ban_image_dir . "{$request['board']}/$hash{$post->ext}");
if (!$has_approved) {
@unlink($ban_thumb_dir . "{$request['board']}/{$hash}s.jpg");
}
// Janitor stats
if (!$has_approved) {
$query = <<<SQL
INSERT INTO ban_request_stats(janitor, approved, denied)
VALUES('%s', 0, 1) ON DUPLICATE KEY UPDATE denied = denied + 1
SQL;
$result = mysql_global_call($query, $request['janitor']);
if (!$result) {
$this->error('Database error (cbr1)');
}
// Janitor stats (2)
$this->update_janitor_stats(
$request['janitor'],
self::STATS_ACTION_DENIED,
$request['board'],
$post->no,
$request['ban_template'],
0,
$_COOKIE['4chan_auser']
);
}
}
private function hard_clear_report($board, $no) {
// Arguments should already be escaped for SQL
$query = "DELETE FROM reports WHERE board = '$board' AND no = $no";
$res = mysql_global_call($query);
if (!$res) {
$this->error("Failed to delete reports for /$board/$no");
}
$query = "DELETE FROM reports_for_posts WHERE board = '$board' AND postid = $no";
$res = mysql_global_call($query);
if (!$res) {
$this->error("Failed to delete reports for /$board/$no");
}
return true;
}
private function should_ban_global($ip) {
$thres = (int)self::LOCAL_TO_GLOBAL_THRES;
if ($thres <= 0) {
return false;
}
$query =<<<SQL
SELECT SQL_NO_CACHE board FROM banned_users
WHERE active = 1 AND host = '%s' AND length > NOW() AND global = 0
GROUP BY board
SQL;
$res = mysql_global_call($query, $ip);
if (!$res) {
return false;
}
if (mysql_num_rows($res) >= $thres) {
return true;
}
return false;
}
private function clear_orphaned_reports() {
$query =<<<SQL
SELECT reports_for_posts.board, reports_for_posts.postid FROM reports_for_posts
LEFT JOIN reports
ON reports.no = reports_for_posts.postid
AND reports.board = reports_for_posts.board
WHERE reports.no IS null
SQL;
$res = mysql_global_call($query);
while ($row = mysql_fetch_assoc($res)) {
$board = $row['board'];
$postid = $row['postid'];
mysql_global_call("DELETE FROM reports_for_posts WHERE board = '$board' AND postid = $postid");
}
return true;
}
public function clear_stale_reports() {
if (isset($_GET['board'])) {
$board = mysql_real_escape_string($_GET['board']);
}
else {
$this->error('Missing board');
}
if (!in_array($board, $this->get_board_list())) {
$this->error('Invalid board');
}
$post_ids = array();
$report_ids = array();
// fetch reports
$query = "SELECT postid FROM reports_for_posts WHERE board = '$board'";
$res = mysql_global_call($query);
while ($row = mysql_fetch_assoc($res)) {
$report_ids[] = $row['postid'];
}
if (empty($report_ids)) {
$this->success();
return;
}
// fetch posts
$query = "SELECT no FROM `$board` WHERE no IN(" . implode(',', $report_ids) . ")";
$res = mysql_board_call($query);
if (mysql_num_rows($res) == count($report_ids)) {
$this->success();
return;
}
// delete reports
while ($row = mysql_fetch_assoc($res)) {
$post_ids[] = $row['no'];
}
$delete_ids = array_diff($report_ids, $post_ids);
$delete_ids = implode(',', $delete_ids);
$query = "DELETE FROM reports WHERE board = '$board' AND no IN($delete_ids)";
mysql_global_call($query);
$query = "DELETE FROM reports_for_posts WHERE board = '$board' AND postid IN($delete_ids)";
mysql_global_call($query);
$del_count = mysql_affected_rows();
$this->success(array('count' => $del_count));
}
// $ids should already be SQL-safe
private function get_report_categories_by_ids($ids) {
$ids = implode(',', $ids);
$sql = "SELECT id, title FROM report_categories WHERE id IN($ids) ORDER BY id ASC";
$res = mysql_global_call($sql);
$data = [];
if (!$res) {
return $data;
}
while ($row = mysql_fetch_assoc($res)) {
$data[$row['id']] = $row['title'];
}
return $data;
}
public function get_templates() {
if (isset($_GET['board'])) {
$board = '/' . $_GET['board'] . '/';
}
else {
$this->error('Missing board');
}
$level_map = get_level_map($this->userLevel);
$query = 'SELECT no, name, rule, bantype, publicreason, days, level, can_warn FROM ban_templates ORDER BY length(rule), rule ASC';
$res = mysql_global_call($query);
if (!$res) {
$this->error('Database error');
}
$templates = array();
while ($tpl = mysql_fetch_assoc($res)) {
if (!isset($level_map[$tpl['level']]) || $level_map[$tpl['level']] !== true) {
continue;
}
if (preg_match('#^(global|' . $_GET['board'] . ')[0-9]+$#', $tpl['rule'])) {
unset($tpl['level']);
unset($tpl['rule']);
$templates[] = $tpl;
}
}
$this->success($templates);
}
// Fetch board specific templates and a selection of global templates
// for use in the quick ban/br function
private function get_quickable_templates($boards) {
$boards_regex = implode('|', $boards);
// no => [boards to skip]
$global_ids = [
6 => false, // Global 5 - NWS on Worksafe Board
222 => ['s4s'], // Global 3 - Troll posts
223 => ['pol'], // Global 3 - Racism
135 => false, // Global 9 - Ban Evasion [Temp]
];
$file_only_tpl = 6;
$ws_only_tpl = 6;
$level_map = get_level_map($this->userLevel);
$query = 'SELECT no, name, rule, level, postban, postban_arg FROM ban_templates ORDER BY length(rule), rule ASC';
$res = mysql_global_call($query);
if (!$res) {
$this->error('Database error');
}
$templates = [];
while ($tpl = mysql_fetch_assoc($res)) {
if (!isset($level_map[$tpl['level']]) || $level_map[$tpl['level']] !== true) {
continue;
}
if ($tpl['postban'] === 'move' && !$tpl['postban_arg']) {
continue;
}
if (isset($global_ids[$tpl['no']]) || preg_match("#^($boards_regex)[0-9]+$#", $tpl['rule'])) {
$board = preg_replace('/^([a-z]+|3)+[0-9]+x?$/', '$1', $tpl['rule']);
$tpl_data = [
'no' => (int)$tpl['no'],
'name' => $tpl['name']
];
if ($tpl['no'] == $file_only_tpl) {
$tpl_data['file_only'] = 1;
}
if ($tpl['no'] == $ws_only_tpl) {
$tpl_data['ws_only'] = 1;
}
$skip_boards = $global_ids[$tpl['no']];
if ($skip_boards) {
$tpl_data['skip'] = $skip_boards;
}
if (!isset($templates[$board])) {
$templates[$board] = [];
}
$templates[$board][] = $tpl_data;
}
}
return $templates;
}
public function count_reports() {
// Access check
if ($this->access['clear'] !== 1) {
$this->error("Can't let you do that");
}
$data = array();
if ($this->access['ban']) {
// User is a mod
// Ban requests
$result = mysql_global_call('SELECT COUNT(*) FROM ban_requests');
$data['banreqs'] = (int)mysql_fetch_row($result)[0];
// Priority ban requests
$in_tpl = array();
$result = mysql_global_call("SELECT no FROM ban_templates WHERE rule = 'global1'");
while ($tid = mysql_fetch_row($result)) {
$in_tpl[] = $tid[0];
}
$in_tpl = implode(',', $in_tpl);
$result = mysql_global_call("SELECT COUNT(*) FROM ban_requests WHERE ban_template IN ($in_tpl)");
$data['illegal_banreqs'] = (int)mysql_fetch_row($result)[0];
// Appeals
$query = <<<SQL
SELECT ban.4pass_id FROM banned_users ban
LEFT OUTER JOIN appeals ON appeals.no = ban.no
WHERE ban.active = 1
AND appeals.closed = 0
AND appeals.updated != 0
AND (ban.length = 0 or ban.length >= NOW())
SQL;
$result = mysql_global_call($query);
$data['appeals'] = mysql_num_rows($result);
$prio_appeal_count = 0;
while ($row = mysql_fetch_row($result)) {
if ($row[0] !== '') {
$prio_appeal_count += 1;
}
}
$data['prio_appeals'] = $prio_appeal_count;
}
// Reports
$result = $this->fetch_report_counts();
if (!$result) {
$this->error('Error while fetching the report count');
}
$report_count = 0;
$report_count_illegal = 0;
$board_coefs = $this->get_board_coefficients();
$hl_thres = $this->isMod ? self::HIGHLIGHT_THRES : self::GLOBAL_THRES;
while ($report = mysql_fetch_assoc($result)) {
$total_weight = (float)$report['total_weight'];
if (isset($board_coefs[$report['board']])) {
$total_weight = $total_weight * $board_coefs[$report['board']];
}
if (!$report['resto']) {
$total_weight = $total_weight * self::THREAD_WEIGHT_BOOST;
}
if ($total_weight >= 1.00) {
$report_count++;
}
if ($total_weight >= $hl_thres) {
$report_count_illegal++;
}
}
$data['total'] = $report_count;
$data['illegal'] = $report_count_illegal;
// Thread flood detection
if ($this->isDeveloper) {
$flood_stats = $this->get_flood_stats();
if ($flood_stats) {
$data['flood'] = $flood_stats;
}
}
$unread_msg = $this->staffmessages();
if ($unread_msg) {
$data['msg'] = $unread_msg;
}
$this->success($data);
}
public function get_ip() {
// Access check
if ($this->access['ban'] !== 1) {
$this->error("Can't let you do that");
}
if (isset($_GET['board'])) {
$board = mysql_real_escape_string($_GET['board']);
}
else {
$this->error('Missing board');
}
if (isset($_GET['no'])) {
$no = (int)$_GET['no'];
}
else {
$this->error('Missing post ID');
}
$post = mysql_board_get_post($board, $no);
if (!$post) {
$this->error("This post doesn't exist anymore");
}
$this->success($post['host']);
}
public function get_ban_requests() {
if ($this->access['ban'] !== 1) {
$this->error("Can't let you do that");
}
if (isset($_GET['board'])) {
$board = "WHERE board = '" . mysql_real_escape_string($_GET['board']) . "'";
$order = "ban_template ASC, ts ASC";
}
else {
$board = "WHERE board != 'test'";
$order = "ts ASC";
}
// Getting templates
$all_templates = array();
$result = mysql_global_call('SELECT * FROM ban_templates');
if (!mysql_num_rows($result)) {
$this->error("Couldn't get ban templates");
}
while ($tpl = mysql_fetch_assoc($result)) {
$all_templates[$tpl['no']] = $tpl;
}
// Getting requests
$query = <<<SQL
SELECT SQL_NO_CACHE id, host, reverse, ban_template, board, janitor, post_json as post, spost,
warn_req, UNIX_TIMESTAMP(ts) as time
FROM ban_requests $board
ORDER BY $order
SQL;
$result = mysql_global_call($query);
$templates = array();
$requests = array();
$cached_ban_history = array();
while ($req = mysql_fetch_assoc($result)) {
if (!isset($templates[$req['ban_template']])) {
$templates[$req['ban_template']] = true;
}
$post = unserialize($req['spost']);
unset($req['spost']);
$link = return_archive_link($req['board'], $post['no'], false, true);
if ($link !== false) {
$req['link'] = $link;
}
// Get ban history
if (isset($cached_ban_history[$req['host']])) {
$ban_history = $cached_ban_history[$req['host']];
}
else {
$ban_history = $this->get_bans_summary($req['host'], $post['4pass_id']);
$cached_ban_history[$req['host']] = $ban_history;
foreach($ban_history['active_bans'] as $_tpl_id) {
$templates[$_tpl_id] = true;
}
foreach($ban_history['active_warns'] as $_tpl_id) {
$templates[$_tpl_id] = true;
}
}
if ($ban_history['total'] > 0) {
$req['ban_history'] = $ban_history;
}
if ($post['4pass_id']) {
$req['pass_user'] = true;
}
$req['pwd'] = $post['pwd'];
// Geo location
$geoinfo = GeoIP2::get_country($req['host']);
if ($geoinfo && isset($geoinfo['country_code'])) {
$geo_loc = array();
if (isset($geoinfo['city_name'])) {
$geo_loc[] = $geoinfo['city_name'];
}
if (isset($geoinfo['state_code'])) {
$geo_loc[] = $geoinfo['state_code'];
}
$geo_loc[] = $geoinfo['country_name'];
$req['geo_loc'] = implode(', ', $geo_loc);
}
// ASN
$asninfo = GeoIP2::get_asn($req['host']);
if ($asninfo) {
$req['asn_name'] = $asninfo['aso'];
}
// Put priority requests on top
if (isset($all_templates[$req['ban_template']])) {
$tpl_rule = $all_templates[$req['ban_template']]['rule'];
}
else {
$tpl_rule = null;
}
if ($tpl_rule === 'global1' || $tpl_rule === 'hm6' || $tpl_rule === 's6') {
array_unshift($requests, $req);
}
else {
$requests[] = $req;
}
}
// TODO: Sort requests by template priority
// Only keep relevant templates
foreach ($templates as $id => &$tpl) {
$tpl = array(
'name' => $all_templates[$id]['name'],
'reason' => $all_templates[$id]['publicreason'],
'is_warn' => !$all_templates[$id]['days'] && !$all_templates[$id]['banlen'],
'is_global' => $all_templates[$id]['bantype'] === 'global'
);
if (!$tpl['is_warn']) {
$tpl['days'] = $all_templates[$id]['days'];
}
if ($all_templates[$id]['rule'] === 'global1') {
$tpl['global1'] = true;
}
}
$counts = $this->count_board_brs();
$this->success(array(
'templates' => $templates,
'requests' => $requests,
'counts' => $counts
));
}
public function reports() {
// Access check
if ($this->access['clear'] !== 1) {
$this->error("Can't let you do that");
}
if ($this->isMod) {
$this->disAccess = false;
$this->boardlist = $this->get_board_list();
}
else {
$this->disAccess = false;
if (!isset($this->access['board'])) {
$this->disAccess = false;
$this->boardlist = $this->get_board_list();
}
else {
if (in_array('dis', $this->access['board'])) {
$this->disAccess = false;
}
$this->boardlist = $this->access['board'];
}
}
$this->renderHTML('reportqueue-test');
}
public function ban_requests() {
// Access check
if ($this->access['ban'] !== 1) {
$this->error("Can't let you do that");
}
$this->boardlist = $this->get_board_list();
$this->renderHTML('banrequests-test');
}
public function get_reporters() {
// Access check
if ($this->access['ban_reporter'] !== 1) {
$this->error("Can't let you do that");
}
if (isset($_GET['no'])) {
$no = (int)$_GET['no'];
}
else {
$this->error('Missing no');
}
if (isset($_GET['board'])) {
$board = mysql_real_escape_string($_GET['board']);
}
else {
$this->error('Missing board');
}
$query = <<<SQL
SELECT SQL_NO_CACHE reports.id, cat, ip, reports.weight, title as cat, UNIX_TIMESTAMP(ts) as time
FROM reports
LEFT JOIN report_categories ON report_categories.id = report_category
WHERE reports.board = '$board'
AND no = $no
ORDER BY ts DESC
SQL;
$result = mysql_global_call($query);
if (mysql_num_rows($result) > 0) {
$data = array();
while ($user = mysql_fetch_assoc($result)) {
$user['ipStr'] = long2ip($user['ip']);
$geoinfo = GeoIP2::get_country($user['ipStr']);
if ($geoinfo && isset($geoinfo['country_code'])) {
$user['country'] = $geoinfo['country_code'];
}
$data[] = $user;
}
}
else {
$this->error('Nothing found');
}
$this->success($data);
}
public function get_rep_details() {
// Access check
if ($this->access['ban_reporter'] !== 1) {
$this->error("Can't let you do that");
}
$has_pass = false;
if (isset($_GET['rid'])) {
$rid = (int)$_GET['rid'];
$query = "SELECT SQL_NO_CACHE ip, 4pass_id, pwd FROM reports WHERE id = $rid LIMIT 1";
$result = mysql_global_call($query);
if (!$result) {
$this->error('Database Error');
}
$report = mysql_fetch_assoc($result);
if (!$report) {
$this->error('Report not found');
}
$ip_str = long2ip($report['ip']);
if (!$ip_str) {
$this->error('Internal Server Error (1)');
}
$clauses = array(
'ip = ' . (int)$report['ip']
);
$ban_clauses = array(
"host = '$ip_str'"
);
if ($report['4pass_id']) {
$pass_id_sql = mysql_real_escape_string($report['4pass_id']);
if (!$pass_id_sql) {
$this->error('Internal Server Error (2)');
}
$clauses[] = "4pass_id = '$pass_id_sql'";
$ban_clauses[] = "4pass_id = '$pass_id_sql'";
$has_pass = true;
}
if ($report['pwd']) {
$pwd_sql = mysql_real_escape_string($report['pwd']);
if (!$pwd_sql) {
$this->error('Internal Server Error (3)');
}
$clauses[] = "pwd = '$pwd_sql'";
$ban_clauses[] = "password = '$pwd_sql'";
}
}
else if (isset($_GET['ip'])) {
$ip = ip2long($_GET['ip']);
if (!$ip) {
$this->error('Invalid IP');
}
$clauses = array(
'ip = ' . $ip
);
}
else {
$this->error('Missing parameters');
}
$clauses = implode(' OR ', $clauses);
$query = <<<SQL
SELECT SQL_NO_CACHE reports.id, reports.board, reports.weight, title as cat, post_json,
UNIX_TIMESTAMP(ts) as time, cleared, no, ip
FROM reports
LEFT JOIN report_categories ON report_categories.id = report_category
WHERE ($clauses)
ORDER BY ts DESC
SQL;
$result = mysql_global_call($query);
if (!$result) {
$this->error('Database Error');
}
$reports = array();
$country_cache = [];
while ($report = mysql_fetch_assoc($result)) {
$report['ip'] = long2ip($report['ip']);
$rep_ip = $report['ip'];
if (isset($country_cache[$rep_ip])) {
$report['country'] = $country_cache[$rep_ip];
}
else {
$geoinfo = GeoIP2::get_country($rep_ip);
if ($geoinfo && isset($geoinfo['country_code'])) {
$report['country'] = $geoinfo['country_code'];
$country_cache[$rep_ip] = $geoinfo['country_code'];
}
}
unset($report['4pass_id']);
unset($report['pwd']);
$reports[] = $report;
}
if (empty($reports)) {
$this->error('Nothing found');
}
$data = array('reports' => $reports);
if (isset($country_cache[$ip_str])) {
$data['country'] = $country_cache[$ip_str];
}
if ($has_pass) {
$data['has_pass'] = true;
}
$this->success($data);
}
public function ban_reporters() {
// Access check
if ($this->access['ban_reporter'] !== 1) {
$this->error("Can't let you do that");
}
// Single reporter ban
if (isset($_POST['rid'])) {
$rid = (int)$_POST['rid'];
if (!$rid) {
$this->error('Bad Request');
}
}
else {
$rid = null;
}
if (isset($_POST['board']) && isset($_POST['pid'])) {
$board = $_POST['board'];
$pid = (int)$_POST['pid'];
if ($board === '' || !$pid) {
$this->error('Bad Request');
}
if (!$this->is_board_valid($board)) {
$this->error('Invalid board');
}
}
else {
$this->error('Missing parameters');
}
// Fetch reported post
$query = "SELECT SQL_NO_CACHE * FROM `%s` WHERE no = $pid LIMIT 1";
$result = mysql_board_call($query, $board);
if (!$result) {
$this->error('Database error (1)');
}
$post = mysql_fetch_assoc($result);
if (!$post) {
$this->error("Couldn't find reported post");
}
if ($post['ext'] !== '') {
$post['filedeleted'] = 1;
}
// Prepare ban/warn template
$tpl_id = (int)self::REP_ABUSE_TPL;
$result = mysql_global_call("SELECT * FROM ban_templates WHERE no = $tpl_id");
if (!mysql_num_rows($result)) {
$this->error("Couldn't get the ban template");
}
$template = mysql_fetch_assoc($result);
$tpl_days = $template['days'];
// Fetch reporters
$query = "SELECT SQL_NO_CACHE ip, pwd, 4pass_id, weight, report_category FROM reports WHERE board = '%s' AND no = $pid";
if ($rid) {
$query .= " AND id = $rid";
}
$result = mysql_global_call($query, $board);
if (!$result) {
$this->error('Database error (2)');
}
$ip_count = mysql_num_rows($result);
if (!$ip_count) {
$this->error("Nothing to do");
}
$is_warn = isset($_POST['warn']) && $_POST['warn'];
$reporters = array();
while ($row = mysql_fetch_assoc($result)) {
$ipStr = long2ip((int)$row['ip']);
if (!$ipStr) {
continue;
}
if ($ip_count < 15) {
$reverse = gethostbyaddr($ipStr);
}
else {
$reverse = $ipStr;
}
$warn_upgraded = false;
if ($is_warn) {
list($warn_count, $ban_count) = $this->get_abuse_bans_summary($ipStr, $row['4pass_id']);
if ($ban_count >= 2) {
$days = $tpl_days;
$warn_upgraded = true;
}
else if ($ban_count > 0 || $warn_count >= 2) {
$days = 1;
$warn_upgraded = true;
}
else {
$days = 0;
}
}
else {
$days = $tpl_days;
}
$reporters[] = array(
'ip' => $ipStr,
'reverse' => $reverse,
'report_cat' => (int)$row['report_category'],
'report_weight' => (int)$row['weight'],
'pwd' => $row['pwd'],
'pass_id' => $row['4pass_id'],
'days' => $days,
'warn_upgraded' => $warn_upgraded
);
}
$rule = $template['rule'];
// Insert bans
$query =<<<SQL
INSERT INTO banned_users (
board, global, zonly, name, host, reverse, xff, reason, length,
admin, md5, post_num, rule, post_time, template_id, password, 4pass_id,
post_json, admin_ip
)
VALUES ('%s', %d, %d, '%s', '%s', '%s', '%s', '%s', '%s',
'%s', '', %d, '%s', '', %d, '%s', '%s',
'%s', '%s')
SQL;
$total = 0;
foreach ($reporters as $rep) {
$length = date('Y-m-d H:i:s', time() + ($rep['days'] * (24 * 60 * 60)));
$ipStr = $rep['ip'];
$reverse = $rep['reverse'];
$post['report_weight'] = $rep['report_weight'];
$post['report_cat'] = $rep['report_cat'];
$pass_id = $rep['pass_id'];
$post_json = json_encode($post, JSON_PARTIAL_OUTPUT_ON_ERROR);
if (json_last_error() !== JSON_ERROR_NONE) {
$this->error('Internal Server Error');
}
$private_reason = [];
if ($template['privatereason']) {
$private_reason[] = $template['privatereason'];
}
if ($rep['warn_upgraded']) {
$private_reason[] = 'auto-upgraded from a warning';
}
$reason = $template['publicreason'] . '<>' . implode(', ', $private_reason);
$result = mysql_global_call($query,
$board, 1, 0, 'Anonymous', $ipStr, $reverse, '', $reason, $length,
$_COOKIE['4chan_auser'], $pid, $rule, $tpl_id, $rep['pwd'], $pass_id,
$post_json, $_SERVER['REMOTE_ADDR']
);
if (!$result) {
$this->error('Database error (i)');
}
$total++;
}
if ($total > 0) {
$this->success("Done. $total IP(s) affected");
}
else {
$this->error('Nothing to do');
}
}
public function clear_reporter() {
// Access check
if ($this->access['clear_reporter'] !== 1) {
$this->error("Can't let you do that");
}
if (isset($_POST['rid'])) {
$rid = (int)$_POST['rid'];
$res = mysql_global_call("SELECT SQL_NO_CACHE ip, pwd, 4pass_id FROM reports WHERE id = $rid LIMIT 1");
if (!$res) {
$this->error('Database error');
}
$report = mysql_fetch_assoc($res);
if (!$report) {
$this->error('Report not found');
}
}
else if (isset($_POST['ip'])) {
$long_ip = ip2long($_POST['ip']);
if (!$long_ip) {
$this->error('Invalid IP');
}
$report = array(
'ip' => $long_ip,
'pwd' => '',
'4pass_id' => ''
);
}
else {
$this->error('Missing report ID');
}
// ---
$clauses = array(
'ip = ' . (int)$report['ip']
);
if ($report['4pass_id']) {
$param = mysql_real_escape_string($report['4pass_id']);
if ($param) {
$clauses[] = "4pass_id = '$param'";
}
}
if ($report['pwd']) {
$param = mysql_real_escape_string($report['pwd']);
if ($param) {
$clauses[] = "pwd = '$param'";
}
}
$clauses = implode(' OR ', $clauses);
mysql_global_call("DELETE FROM reports WHERE $clauses");
$affected = mysql_affected_rows();
if ($affected === -1) {
$this->error('Database error');
}
else {
$this->clear_orphaned_reports();
/*
$this->log_cleared_reporter(
$report['ip'],
$report['pwd'],
$report['4pass_id'],
0, 1.0
);
*/
$this->success(array('affected' => $affected));
}
}
public function accept_ban_request() {
// Access check
if ($this->access['ban'] !== 1) {
$this->error("Can't let you do that");
}
// $this->error('Disabled');
if (isset($_POST['id'])) {
$id = (int)$_POST['id'];
}
else {
$this->error('Missing ID');
}
// Fetching the request
$query = "SELECT * FROM ban_requests WHERE id = {$id}";
$queries[] = $query;
$result = mysql_global_call($query);
if (!mysql_num_rows($result)) {
$this->error("This request doesn't exist anymore", 404);
}
$request = mysql_fetch_assoc($result);
$amend_length = false;
// Fetching ban template
if (isset($_POST['amend_tpl'])) {
$req_amend = true;
$template_id = (int)$_POST['amend_tpl'];
if (isset($_POST['ban_length'])) {
$amend_length = (int)$_POST['ban_length'];
if ($amend_length < -1 || $amend_length > 1024) {
$this->error('Invalid length');
}
}
}
else {
$req_amend = false;
$template_id = (int)$request['ban_template'];
}
if ($amend_length !== false && $amend_length === 0) {
$warn = true;
}
else if (!$req_amend && $request['warn_req']) {
$warn = true;
}
else {
$warn = false;
}
// Don't allow pass revoke templates
if (!$template_id || in_array($template_id, array(130, 131))) {
$this->error('Invalid template');
}
$query = "SELECT * FROM ban_templates WHERE no = $template_id";
$result = mysql_global_call($query);
if (!mysql_num_rows($result)) {
$this->error("Couldn't get the ban template");
}
$template = mysql_fetch_assoc($result);
$level_map = get_level_map($this->userLevel);
if ($level_map[$template['level']] !== true) {
$this->error('You cannot use this template');
}
// Inserting the ban
$post = json_decode($request['post_json']);
$spost = unserialize($request['spost']);
$pass_id = $spost['4pass_id'];
$board = $request['board'];
if ($warn) {
$length = date( "Y-m-d H:i:s", time());
}
else if ($amend_length !== false) {
if ($amend_length < 0) {
$length = '0000-00-00 00:00:00';
}
else {
$length = date( "Y-m-d H:i:s", time() + ($amend_length * (24 * 60 * 60)));
}
}
else if ($template['banlen'] == 'indefinite') {
$length = '0000-00-00 00:00:00';
}
else {
if ($req_amend && $template['days'] === '0' && !$warn) {
$tpl_days = 1;
}
else {
$tpl_days = $template['days'];
}
$length = date( "Y-m-d H:i:s", time() + ($tpl_days * (24 * 60 * 60)));
}
if ($req_amend && !$warn && isset($_POST['global'])) {
$global = $_POST['global'] === '1';
}
else {
$global = $template['bantype'] == 'global';
}
// Delete the file if the request was amended to a template with save_json = json_only
if ($req_amend && $post->ext && $template['save_post'] == 'json_only') {
$salt = file_get_contents('/www/keys/legacy.salt');
$hash = sha1($request['board'] . $post->no . $salt);
$ban_thumb_dir = '/www/4chan.org/web/images/bans/thumb/';
$ban_image_dir = '/www/4chan.org/web/images/bans/src/';
unlink($ban_image_dir . "{$request['board']}/$hash{$post->ext}");
if (!$has_approved) {
unlink($ban_thumb_dir . "{$request['board']}/{$hash}s.jpg");
}
$post->raw_md5 = $spost['md5'];
}
if ($req_amend && isset($_POST['public_reason']) && $_POST['public_reason'] !== '') {
$public_reason = nl2br(htmlspecialchars($_POST['public_reason']), false);
}
else {
$public_reason = nl2br($template['publicreason'], false);
}
if ($template['privatereason']) {
$reason = "{$public_reason}<>{$template['privatereason']}, ban requested by {$request['janitor']}";
}
else {
$reason = "{$public_reason}<>ban requested by {$request['janitor']}";
}
if ($req_amend && isset($_POST['private_reason']) && $_POST['private_reason'] !== '') {
$reason .= '. ' . htmlspecialchars($_POST['private_reason'], ENT_QUOTES);
}
$rule = $template['rule'];
$name = $spost['name'];
$tripcode = '';
$name_bits = explode('</span> <span class="postertrip">!', $name);
if ($name_bits[1]) {
$tripcode = preg_replace('/<[^>]+>/', '', $name_bits[1]); // fixme: why do we do that?
}
else {
$tripcode = '';
}
$name = str_replace('</span> <span class="postertrip">!', ' #', $name);
$name = preg_replace('/<[^>]+>/', '', $name);
if ($request['host'] != '') {
// Check if ban should be changed to global
if (!$req_amend && !$warn && !$global) {
if ($this->should_ban_global($request['host'])) {
$global = true;
}
}
// FIXME: email field
if (isset($spost['email'])) {
$spost['ua'] = $spost['email'];
unset($spost['email']);
}
$post_json = json_encode($spost);
$result = mysql_global_call(
"INSERT INTO " . SQLLOGBAN . " (
board, global, zonly, name, host, reverse, xff, reason, length, admin,
md5, post_num, rule, post_time, template_id, post_json, 4pass_id, admin_ip,
tripcode, password
)
VALUES ('%s', %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d,
'%s', '%s', %d, '%s', '%s', '%s', '%s', '%s');",
$board, $global, 0, $name, $request['host'], $request['reverse'], $request['xff'], $reason,
$length, $_COOKIE['4chan_auser'], $spost['md5'], $post->no, $rule, $post->time, $template_id,
$post_json, $pass_id, $_SERVER['REMOTE_ADDR'], $tripcode, $spost['pwd']
);
if (!$result) {
$this->error('Database error (abr0)');
}
$ban_id = mysql_global_insert_id();
$ban_string = $global ? 'global' : $board;
// Delete all by ip
if (!$warn && $template['postban'] == 'delall') {
//$this->delete_by_ip($request['board'], $request['host']);
}
}
else {
$ban_id = 0;
}
// Blacklisting
if (!$warn && ($template['blacklist'] == 'image' || $template['blacklist'] == 'rejectimage') && $post->raw_md5) {
global $gcon;
$inserted_ban_id = mysql_insert_id($gcon);
$bl_ban = $template['blacklist'] == 'image' ? '1' : '0';
$query = <<<SQL
INSERT INTO blacklist(field, contents, description, addedby, ban, banlength, banreason)
VALUES('md5','%s','%s','%s','$bl_ban','-1','%s')
SQL;
$result = mysql_global_call($query,
$post->raw_md5,
"{$template['publicreason']} (via ban template, ban requested by {$request['janitor']}, ban id: $inserted_ban_id)",
$_COOKIE['4chan_auser'],
$template['publicreason']
);
if (!$result) {
$this->error('Database error (abr1)');
}
}
// Janitor stats
$query = <<<SQL
INSERT INTO ban_request_stats(janitor, approved, denied)
VALUES('%s', 1, 0) ON DUPLICATE KEY UPDATE approved = approved + 1
SQL;
$result = mysql_global_call($query, $request['janitor']);
if (!$result) {
$this->error('Database error (abr2)');
}
// Clearing
$salt = file_get_contents('/www/keys/legacy.salt');
$this->clear_ban_request($request['id'], $salt, true);
// Remove duplicate ban requests
if ($template['save_post'] != 'json_only') {
if ($warn) {
$warn_clause = 'AND warn_req = 1';
}
else {
$warn_clause = '';
}
$query = "SELECT id FROM ban_requests WHERE host = '%s' AND ban_template = %d $warn_clause";
$result = mysql_global_call($query, $request['host'], $template_id);
while ($row = mysql_fetch_assoc($result)) {
$this->clear_ban_request($row['id'], $salt, true);
}
}
// Janitor stats (2)
$this->update_janitor_stats(
$request['janitor'],
self::STATS_ACTION_ACCEPTED,
$request['board'],
$post->no,
$request['ban_template'],
$template_id,
$_COOKIE['4chan_auser']
);
$request_ids = $this->get_request_ids();
$this->success(array('ban_id' => $ban_id, 'request_ids' => $request_ids));
}
public function deny_ban_request() {
// Access check
if ($this->access['ban'] !== 1) {
$this->error("Can't let you do that");
}
// $this->error('Disabled');
if (isset($_POST['id'])) {
$id = (int)$_POST['id'];
}
else {
$this->error('Missing ID');
}
$salt = file_get_contents('/www/keys/legacy.salt');
$result = $this->clear_ban_request($id, $salt);
$request_ids = $this->get_request_ids();
$this->success(array('request_ids' => $request_ids));
}
public function get_reports() {
// Access check
if ($this->access['clear'] !== 1) {
$this->error("Can't let you do that");
}
$board = false;
$ws_mode = self::WS_ANY;
if (isset($_GET['board'])) {
if ($_GET['board'] === '_ws_') {
$ws_mode = self::WS_ONLY;
}
else if ($_GET['board'] === '_nws_') {
$ws_mode = self::WS_NOT;
}
else {
$board = $_GET['board'];
}
if ($board == 'test' && !$this->isDeveloper) {
$this->error("Can't let you do that");
}
}
$cleared_only = isset($_GET['cleared_only']);
$ignored_only = isset($_GET['ignored']) && $this->access['ban'] === 1;
if ($board === false && $cleared_only) {
$this->error('Cleared Queue is disabled for this set of filters');
}
$reports = array();
$needs_cleared_by = $cleared_only && $this->isMod;
$board_coefs = $this->get_board_coefficients();
$result = $this->fetch_reports($board, $cleared_only, $ws_mode);
$count = 0;
$uid_idx = 1;
$uid_map = array();
$need_rep_cats = $this->isMod;
if ($need_rep_cats) {
$rep_cat_ids = [];
}
$reported_boards = [];
if (mysql_num_rows($result) > 0) {
while ($report = mysql_fetch_assoc($result)) {
if ($r = $this->build_report($report)) {
if ($count >= self::MAX_REPORTS_PER_PAGE) {
break;
}
if ($needs_cleared_by) {
$r['cleared_by'] = $report['cleared_by'];
}
if (isset($board_coefs[$r['board']]) && !$report['num_illegal']) {
$r['weight'] = $r['weight'] * $board_coefs[$r['board']];
}
if (!$report['resto']) {
$r['weight'] = $r['weight'] * self::THREAD_WEIGHT_BOOST;
}
if ($r['weight'] < 1.00) {
if ($ignored_only) {
$r['weight'] = 0;
$count++;
$reports[] = $r;
}
continue;
}
else if ($ignored_only) {
continue;
}
$r['weight'] = ceil($r['weight']);
// Mods only data
if ($this->isMod) {
$long_ip = (int)$report['post_ip'];
if ($long_ip) {
$uid = $uid_map[$long_ip];
if ($uid) {
$r['uid'] = $uid;
}
else {
$uid_map[$long_ip] = $uid_idx;
$r['uid'] = $uid_idx;
$uid_idx++;
}
}
// Report categories
$r['cats'] = $report['cats'];
if ($need_rep_cats) {
$rep_cat_ids[] = $report['cats'];
}
}
$reported_boards[$r['board']] = true;
$reports[] = $r;
$count++;
}
}
}
if ($count > 0) {
$ban_templates = $this->get_quickable_templates(array_keys($reported_boards));
}
else {
$ban_templates = null;
}
usort($reports, array($this, 'sort_reports_func'));
if (!$this->isMod) {
foreach ($reports as &$r) {
if ($r['weight'] >= self::HIGHLIGHT_THRES) {
$r['hl'] = 1;
}
unset($r['weight']);
unset($r['count']);
}
}
if (!$board && $ws_mode == self::WS_ANY) {
$counts = [];
foreach ($reports as &$r) {
if (!isset($counts[$r['board']])) {
$counts[$r['board']] = 1;
continue;
}
$counts[$r['board']]++;
}
}
else {
$counts = $this->count_board_reports();
}
$data = array(
'reports' => $reports,
'counts' => $counts,
'templates' => $ban_templates
);
if ($need_rep_cats && !empty($rep_cat_ids)) {
$rep_cat_ids = implode(',', $rep_cat_ids);
$rep_cat_ids = explode(',', $rep_cat_ids);
$rep_cat_ids = array_unique($rep_cat_ids);
$rep_cats = $this->get_report_categories_by_ids($rep_cat_ids);
$data['rep_cats'] = $rep_cats;
}
$this->success($data, true);
}
private function sort_reports_func($a, $b) {
if ($a['weight'] == $b['weight']) {
if ($a['ts'] == $b['ts']) {
return 0;
}
return ($a['ts'] > $b['ts']) ? -1 : 1;
}
return ($a['weight'] > $b['weight']) ? -1 : 1;
}
public function get_report_ids() {
// Access check
if ($this->access['clear'] !== 1) {
$this->error("Can't let you do that");
}
$ws_mode = self::WS_ANY;
$board = false;
if (isset($_GET['board'])) {
if ($_GET['board'] === '_ws_') {
$ws_mode = self::WS_ONLY;
}
else if ($_GET['board'] === '_nws_') {
$ws_mode = self::WS_NOT;
}
else {
$board = $_GET['board'];
}
}
$board_clause = '';
// local janitor
if (isset($this->access['board'])) {
if (!$board) {
$board_clause = array();
foreach($this->access['board'] as $b) {
$board_clause[] = mysql_real_escape_string($b);
}
$board_clause = " WHERE board IN('" . implode("','", $board_clause) . "')";
}
else if ($this->can_access_board($board)) {
$board_clause = " WHERE board = '" . mysql_real_escape_string($board) . "'";
}
else {
$this->error("You can't view reports for this board.");
}
}
// mod or global janitor
else {
if ($board) {
$board_clause = " WHERE board = '" . mysql_real_escape_string($board) . "'";
}
}
if ($ws_mode !== self::WS_ANY) {
if ($board_clause) {
$board_clause .= ' AND';
}
else {
$board_clause .= ' WHERE';
}
if ($ws_mode === self::WS_ONLY) {
$board_clause .= ' ws = 1';
}
else if ($ws_mode === self::WS_NOT) {
$board_clause .= ' ws = 0';
}
}
$reports = array();
$query = "SELECT board, no FROM reports$board_clause GROUP BY board, no";
$result = mysql_global_call($query);
if (mysql_num_rows($result) > 0) {
while ($report = mysql_fetch_row($result)) {
$reports[] = $report;
}
}
$counts = $this->count_board_reports();
$data = array(
'reports' => $reports,
'counts' => $counts
);
$this->success($data);
}
public function clear_report() {
// Access check
if ($this->access['clear'] !== 1) {
$this->error("Can't let you do that");
}
if (isset($_POST['no'])) {
$no = (int)$_POST['no'];
}
else {
$this->error('Missing post number');
}
if (isset($_POST['board'])) {
$board = mysql_real_escape_string($_POST['board']);
if (!$this->can_access_board($_POST['board'])) {
// User can't access this board
// Let's see if the post has enough illegal reports
if ($this->is_report_unlocked($board, $no) === false) {
// Nope
$this->error("You don't have access to this board.");
}
}
}
else {
$this->error('Missing board');
}
// Fetching report
$query = "SELECT SQL_NO_CACHE id, post_json, cleared, SUM(weight) as weight FROM reports WHERE board = '$board' AND no = $no";
$res = mysql_global_call($query);
if (!$res || mysql_num_rows($res) !== 1) {
$this->error("Couldn't retrieve report for /$board/$no");
}
$report = mysql_fetch_assoc($res);
if ($report['weight'] == 0) {
$this->error('You cannot clear this report');
}
if ($report['cleared'] === '1') {
// Allow clearing of orphaned reports
$query = "SELECT no FROM `$board` WHERE no = $no";
$res = mysql_board_call($query);
if (!mysql_num_rows($res)) {
$this->hard_clear_report($board, $no);
$this->success('Orphaned report cleared');
return;
}
else {
$this->error('This report is already cleared');
}
}
// Janitors can't clear reports for their own posts
if (!$this->access['ban'] && $this->is_report_for_own_post($board, $no)) {
//$this->error('You cannot clear this report');
//$this->success();
//return;
$this->log_self_clear($board, $no, $report['post_json']);
}
// Fetch all reporters for logging
$all_reporters = array();
$query = "SELECT SQL_NO_CACHE ip, pwd, 4pass_id, report_category, weight FROM reports WHERE board = '$board' AND no = $no AND cleared = 0";
$res = mysql_global_call($query);
if ($res) {
while ($row = mysql_fetch_assoc($res)) {
$all_reporters[] = $row;
}
}
$user = mysql_real_escape_string($_COOKIE['4chan_auser']);
if ($report['post_json']) {
$post_json = json_decode($report['post_json'], true);
$has_img = $post_json['fsize'] > 0 ? 1 : 0;
if ($post_json['trip']) {
$name = $post_json['name'] . '#' . substr($post_json['trip'], 1);
}
else {
$name = $post_json['name'];
}
$name = mysql_real_escape_string($name);
$post_json['com'] = mysql_real_escape_string($post_json['com']);
$post_json['sub'] = mysql_real_escape_string($post_json['sub']);
$post_json['filename'] = mysql_real_escape_string($post_json['filename']);
$post_resto = (int)$post_json['resto'];
$query = <<<SQL
INSERT INTO del_log (postno, board, name, sub, com, img, filename, admin, admin_ip, cleared, resto)
VALUES(
$no,
'$board',
'$name',
'{$post_json['sub']}',
'{$post_json['com']}',
'$has_img',
'{$post_json['filename']}{$post_json['ext']}',
'$user',
'{$_SERVER['REMOTE_ADDR']}',
1,
$post_resto
)
SQL;
}
else {
$query = <<<SQL
INSERT INTO del_log (postno, board, admin, admin_ip, cleared)
VALUES($no, '$board', '$user', '{$_SERVER['REMOTE_ADDR']}', 1)
SQL;
}
// Logging
$res = mysql_global_call($query);
if (!$res) {
$this->error("Failed to clear reports for /$board/$no (1)");
}
// Updating reports table
$query = "UPDATE reports SET cleared = 1, cleared_by = '$user' WHERE board = '$board' AND no = $no";
$res = mysql_global_call($query);
if (!$res) {
$this->error("Failed to clear reports for /$board/$no (2)");
}
// Updating reports_for_posts table
$query = <<<SQL
UPDATE reports_for_posts
SET cleared = 1, clearedby = '$user'
WHERE board = '$board' AND postid = $no
SQL;
$res = mysql_global_call($query);
if (!$res) {
$this->error("Failed to clear reports for /$board/$no (3)");
}
$this->success();
fastcgi_finish_request();
// Logging reporters and checking for abuse
foreach ($all_reporters as $reporter) {
$this->log_cleared_reporter(
$reporter['ip'], $reporter['pwd'], $reporter['4pass_id'],
$reporter['report_category'], $reporter['weight']
);
$this->enforce_cleared_abuse(
$reporter['ip'], $reporter['pwd'], $reporter['4pass_id']
);
}
$this->prune_clear_log();
}
/**
* Renders the staff messages page or returns the number of unread messages
* if called from the count_reports action
*/
public function staffmessages() {
if ($this->access['clear'] !== 1) {
$this->error("Can't let you do that");
}
if ($this->isCountReports) {
$count_mode = true;
$msg_count = 0;
if (isset($_COOKIE['smts'])) {
$ts = (int)$_COOKIE['smts'];
}
else {
$ts = 0;
}
$sql = "SELECT boards FROM staff_messages WHERE created_on > $ts";
}
else {
$count_mode = false;
$lim = self::PAGE_SIZE + 1;
if (isset($_GET['offset'])) {
$offset = (int)$_GET['offset'];
}
else {
$offset = 0;
}
$sql = "SELECT * FROM staff_messages ORDER BY id DESC LIMIT $offset,$lim";
$this->messages = array();
}
$res = mysql_global_call($sql);
if (!$res) {
$this->error('Database error.');
}
if (!$count_mode) {
$this->offset = $offset;
$this->previous_offset = $offset - self::PAGE_SIZE;
if ($this->previous_offset < 0) {
$this->previous_offset = 0;
}
if (mysql_num_rows($res) === $lim) {
$this->next_offset = $offset + self::PAGE_SIZE;
}
else {
$this->next_offset = 0;
}
}
while ($row = mysql_fetch_assoc($res)) {
$flag = false;
// No board restrictions or user is a mod
if ($row['boards'] === '' || $this->isMod) {
$flag = true;
}
// Multiple boards
else if (strpos($row['boards'], ',') !== false) {
$boards = explode(',', $row['boards']);
foreach ($boards as $b) {
if ($this->can_access_board($b)) {
$flag = true;
break;
}
}
}
// Single board
else if ($this->can_access_board($row['boards'])) {
$flag = true;
}
if ($flag) {
if ($count_mode) {
$msg_count++;
}
else {
$this->messages[] = $row;
}
}
}
if ($count_mode) {
return $msg_count;
}
if ($this->next_offset) {
array_pop($this->messages);
}
if (isset($_COOKIE['smts'])) {
$this->ts = (int)$_COOKIE['smts'];
}
else {
$this->ts = false;
}
setcookie('smts',
$_SERVER['REQUEST_TIME'],
$_SERVER['REQUEST_TIME'] + 365 * 24 * 3600,
'/',
'reports.4chan.org',
false,
true
);
$this->renderHTML('messages');
}
// Checks the event log for thread flood alerts to show inside mod tools.
private function get_flood_stats() {
$sql =<<<SQL
SELECT board, COUNT(*) as cnt FROM `event_log`
WHERE type = 'block_flood_check' AND thread_id = 0
AND created_on > DATE_SUB(NOW(), INTERVAL 30 MINUTE)
AND board NOT IN('pol', 'biz', 'v', 'sp')
GROUP BY board
SQL;
$res = mysql_global_call($sql);
if (!$res) {
return false;
}
$data = [];
while ($row = mysql_fetch_row($res)) {
if ($row[1] >= 3) {
$data[] = $row[0];
}
}
if (empty($data)) {
return null;
}
return $data;
}
public function run() {
$this->validate_csrf();
if ($_SERVER['HTTP_HOST'] !== 'reports.4chan.org') {
$this->error('Bad request');
}
if ($this->isCountReports) {
$action = 'count_reports';
}
else if (isset($_REQUEST['action'])) {
$action = $_REQUEST['action'];
}
else {
$action = 'reports';
}
if (in_array($action, $this->actions)) {
$this->$action();
}
else {
$this->error('Bad request');
}
}
}
?>