forked from GithubBackups/vichan
Merge pull request #923 from perdedora/new-filtersystem
New filter system
This commit is contained in:
commit
94082cf517
92
inc/Controller/FloodManager.php
Normal file
92
inc/Controller/FloodManager.php
Normal file
@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace Vichan\Controller;
|
||||
|
||||
use Vichan\Data\Driver\LogDriver;
|
||||
use Vichan\Data\IpNoteQueries;
|
||||
use Vichan\Service\FilterService;
|
||||
use Vichan\Service\FloodService;
|
||||
|
||||
/**
|
||||
* Manages the overall process of checking posts against filters and flood controls.
|
||||
* Orchestrates FilterService and FloodService for incoming posts.
|
||||
*/
|
||||
class FloodManager {
|
||||
/**
|
||||
* @var FilterService Service to apply configured filters to posts.
|
||||
*/
|
||||
private FilterService $filterService;
|
||||
|
||||
/**
|
||||
* @var FloodService Service to record post history for flood detection.
|
||||
*/
|
||||
private FloodService $floodService;
|
||||
|
||||
/**
|
||||
* @var IpNoteQueries Handles database operations for IP notes.
|
||||
*/
|
||||
private IpNoteQueries $notes;
|
||||
|
||||
/**
|
||||
* @var LogDriver Logger instance for recording errors.
|
||||
*/
|
||||
private LogDriver $logger;
|
||||
|
||||
/**
|
||||
* Constructor for FloodManager.
|
||||
*
|
||||
* @param FilterService $filterService The filter checking service.
|
||||
* @param FloodService $floodService The flood recording and checking service.
|
||||
* @param IpNoteQueries $notes The IP note database query handler.
|
||||
* @param LogDriver $logger The logging service.
|
||||
*/
|
||||
public function __construct(
|
||||
FilterService $filterService,
|
||||
FloodService $floodService,
|
||||
IpNoteQueries $notes,
|
||||
LogDriver $logger
|
||||
) {
|
||||
$this->filterService = $filterService;
|
||||
$this->floodService = $floodService;
|
||||
$this->notes = $notes;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes an incoming post through the filter and flood system.
|
||||
*
|
||||
* Applies filters first. If a filter matches, returns the filter result
|
||||
* If no filter matches, records a flood entry, purges old entries, and returns null.
|
||||
* Handles critical errors internally.
|
||||
*
|
||||
* @param array<string, mixed> $post The post data array.
|
||||
* @return array|null The matching filter configuration if found, otherwise null.
|
||||
* Returns null also if a critical error occurs during processing.
|
||||
*/
|
||||
public function processPost(array $post): ?array {
|
||||
try {
|
||||
$filterResult = $this->filterService->applyFilters($post);
|
||||
|
||||
if ($filterResult !== null) {
|
||||
if (isset($filterResult['add_note']) && $filterResult['add_note']) {
|
||||
$this->notes->add(
|
||||
$post['ip'], -1, 'Autoban message: ' . $post['body']
|
||||
);
|
||||
}
|
||||
return $filterResult;
|
||||
}
|
||||
|
||||
$this->floodService->recordFloodEntry($post);
|
||||
|
||||
$this->floodService->purgeOldEntries();
|
||||
|
||||
return null;
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->log(
|
||||
LogDriver::ERROR,
|
||||
"Critical error in flood filtering system: " . $e->getMessage()
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
96
inc/Data/FloodQueries.php
Normal file
96
inc/Data/FloodQueries.php
Normal file
@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace Vichan\Data;
|
||||
|
||||
/**
|
||||
* Handles direct database interactions for the `flood` table.
|
||||
*/
|
||||
class FloodQueries {
|
||||
/**
|
||||
* @var \PDO The PDO database connection instance.
|
||||
*/
|
||||
private \PDO $pdo;
|
||||
|
||||
/**
|
||||
* Constructor for FloodQueries.
|
||||
*
|
||||
* @param \PDO $pdo An active PDO database connection.
|
||||
*/
|
||||
public function __construct(\PDO $pdo) {
|
||||
$this->pdo = $pdo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves recent flood entries matching IP, post hash, or optionally file hash if a post has files.
|
||||
*
|
||||
* @param string $ip The poster's IP address.
|
||||
* @param string $postHash The hash of the post body content.
|
||||
* @param string|null $fileHash Optional hash of the attached file.
|
||||
* @return array<int, array<string, mixed>> An array of matching flood records.
|
||||
*/
|
||||
public function getRecentFloodEntries(string $ip, string $postHash, ?string $fileHash = null): array {
|
||||
$sql = "SELECT * FROM `flood` WHERE `ip` = :ip OR `posthash` = :posthash";
|
||||
|
||||
if ($fileHash) {
|
||||
$sql .= " OR `filehash` = :filehash";
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindValue(':ip', $ip);
|
||||
$stmt->bindValue(':posthash', $postHash);
|
||||
|
||||
if ($fileHash) {
|
||||
$stmt->bindValue(':filehash', $fileHash);
|
||||
}
|
||||
|
||||
$stmt->execute();
|
||||
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new record into the flood table.
|
||||
*
|
||||
* @param string $ip The poster's IP address.
|
||||
* @param string $posthash The hash of the post body content.
|
||||
* @param string|null $filehash The hash of the attached file, or null if post has no files.
|
||||
* @param string $board The board URI where the post was made.
|
||||
* @param int $time The unix timestamp of when the post was made.
|
||||
* @param bool $isreply True if the post was a reply, false if it was a new thread.
|
||||
*/
|
||||
public function addFloodEntry(
|
||||
string $ip,
|
||||
string $posthash,
|
||||
?string $filehash,
|
||||
string $board,
|
||||
int $time,
|
||||
bool $isreply
|
||||
): void {
|
||||
$sql = "INSERT INTO `flood` (`ip`, `posthash`, `filehash`, `board`, `time`, `isreply`)
|
||||
VALUES (:ip, :posthash, :filehash, :board, :time, :isreply)";
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindValue(':ip', $ip);
|
||||
$stmt->bindValue(':posthash', $posthash);
|
||||
$stmt->bindValue(':filehash', $filehash, $filehash === null ? \PDO::PARAM_NULL : \PDO::PARAM_STR);
|
||||
$stmt->bindValue(':board', $board);
|
||||
$stmt->bindValue(':time', $time, \PDO::PARAM_INT);
|
||||
$stmt->bindValue(':isreply', $isreply, \PDO::PARAM_BOOL);
|
||||
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes flood records older than a specified time window.
|
||||
*
|
||||
* @param int $maxTime The maximum age (in seconds) an entry should have to be kept.
|
||||
* Entries older than (current_time - maxTime) will be deleted.
|
||||
*/
|
||||
public function purgeOldEntries(int $maxTime): void {
|
||||
$time = \time() - $maxTime;
|
||||
$sql = "DELETE FROM `flood` WHERE `time` < :time";
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindValue(':time', $time, \PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
}
|
||||
}
|
500
inc/Service/FilterService.php
Normal file
500
inc/Service/FilterService.php
Normal file
@ -0,0 +1,500 @@
|
||||
<?php
|
||||
|
||||
namespace Vichan\Service;
|
||||
|
||||
use Throwable;
|
||||
use Vichan\Data\Driver\LogDriver;
|
||||
|
||||
/**
|
||||
* Applies filters to posts based on configured conditions.
|
||||
*/
|
||||
class FilterService {
|
||||
/**
|
||||
* @var array<int, array<string, mixed>> Loaded filter configurations.
|
||||
*/
|
||||
private array $filters;
|
||||
|
||||
/**
|
||||
* @var FloodService Handles retrieval of flood data and checking flood conditions for filters.
|
||||
*/
|
||||
private FloodService $floodService;
|
||||
|
||||
/**
|
||||
* @var LogDriver Logger instance.
|
||||
*/
|
||||
private LogDriver $logger;
|
||||
|
||||
|
||||
/**
|
||||
* Filter service constructor
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $filters The config filters.
|
||||
* @param FloodService $floodService The FloodService.
|
||||
* @param LogDriver $logger The LogDriver.
|
||||
*/
|
||||
public function __construct(array $filters, FloodService $floodService, LogDriver $logger) {
|
||||
$this->filters = $filters;
|
||||
$this->floodService = $floodService;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies all filters to a post and returns the first matching filter.
|
||||
*
|
||||
* @param array<string, mixed> $post The post array.
|
||||
* @return array|null The matching filter configuration or null if no filter matched.
|
||||
* @throws \Throwable Only if there's a critical error that can't be handled internally
|
||||
*/
|
||||
public function applyFilters(array $post): ?array {
|
||||
$floodEntries = $this->floodService->getFloodEntries($post);
|
||||
|
||||
foreach ($this->filters as $index => $filter) {
|
||||
try {
|
||||
if ($this->checkFilter($filter['condition'], $post, $floodEntries)) {
|
||||
return $filter;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->log(
|
||||
LogDriver::ERROR,
|
||||
"Filter #{$index} failed: {$e->getMessage()}\nDetails: " .
|
||||
\json_encode($filter, \JSON_PRETTY_PRINT)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a post matches all conditions defined within a single filter.
|
||||
*
|
||||
* @param array<string, mixed> $conditions The conditions for this specific filter.
|
||||
* @param array $post The post data array.
|
||||
* @param array<int, array<string, mixed>> $floodEntries Recent post history for flood checks.
|
||||
* @return bool True if the post matches all conditions, false otherwise.
|
||||
* @throws \RuntimeException If evaluation of a specific condition fails.
|
||||
*/
|
||||
private function checkFilter(array $conditions, array $post, array $floodEntries): bool {
|
||||
foreach ($conditions as $condition => $value) {
|
||||
try {
|
||||
$this->validateType($condition, 'string', 'filter condition');
|
||||
|
||||
$negate = $condition[0] === '!';
|
||||
$conditionName = $negate ? \substr($condition, 1) : $condition;
|
||||
|
||||
if (empty($conditionName)) {
|
||||
throw new \InvalidArgumentException("Empty condition name after negation");
|
||||
}
|
||||
|
||||
$result = $this->matchCondition($conditionName, $value, $post, $floodEntries, $conditions);
|
||||
|
||||
if ($result === $negate) {
|
||||
return false;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
throw new \RuntimeException(
|
||||
"Error in condition '$condition': {$e->getMessage()}",
|
||||
0,
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a value is of a expected type.
|
||||
*
|
||||
* @param mixed $value The value to check.
|
||||
* @param string $expectedType The required type ('string', 'array', 'numeric').
|
||||
* @param string $fieldName A descriptive name of the field/value being checked for error messages.
|
||||
* @throws \InvalidArgumentException If the type of $value does not match $expectedType.
|
||||
*/
|
||||
private function validateType(mixed $value, string $expectedType, string $fieldName): void {
|
||||
$isValid = false;
|
||||
|
||||
switch ($expectedType) {
|
||||
case 'string':
|
||||
$isValid = \is_string($value);
|
||||
break;
|
||||
case 'array':
|
||||
$isValid = \is_array($value);
|
||||
break;
|
||||
case 'numeric':
|
||||
$isValid = \is_numeric($value);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$isValid) {
|
||||
throw new \InvalidArgumentException(
|
||||
"Filter configuration error: '$fieldName' must be type '{$expectedType}', but got '" . \gettype($value) . "'."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a regex match and throws an exception on PCRE errors.
|
||||
*
|
||||
* @param string $pattern The regex pattern.
|
||||
* @param string $subject The string to match against.
|
||||
* @param string $fieldName Contextual name for error messages (e.g., 'IP', 'body').
|
||||
* @return bool True if the pattern matches, false otherwise.
|
||||
* @throws \InvalidArgumentException If the regex pattern is invalid.
|
||||
*/
|
||||
private function checkRegex(string $pattern, string $subject, string $fieldName): bool {
|
||||
$result = @\preg_match($pattern, $subject);
|
||||
|
||||
if ($result === false) {
|
||||
throw new \InvalidArgumentException(
|
||||
"Invalid regex pattern for '{$fieldName}' ('{$pattern}'): " . $this->getPregErrorMessage()
|
||||
);
|
||||
}
|
||||
|
||||
return (bool)$result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-readable description of the last regex error.
|
||||
*
|
||||
* @return string The error description.
|
||||
*/
|
||||
private function getPregErrorMessage(): string {
|
||||
$error = \preg_last_error();
|
||||
switch ($error) {
|
||||
case \PREG_NO_ERROR:
|
||||
return "No error";
|
||||
case \PREG_INTERNAL_ERROR:
|
||||
return "Internal PCRE error";
|
||||
case \PREG_BACKTRACK_LIMIT_ERROR:
|
||||
return "Backtrack limit exceeded";
|
||||
case \PREG_RECURSION_LIMIT_ERROR:
|
||||
return "Recursion limit exceeded";
|
||||
case \PREG_BAD_UTF8_ERROR:
|
||||
return "Invalid UTF-8 data";
|
||||
case \PREG_BAD_UTF8_OFFSET_ERROR:
|
||||
return "Invalid UTF-8 offset";
|
||||
case \PREG_JIT_STACKLIMIT_ERROR:
|
||||
return "JIT stack limit exceeded";
|
||||
default:
|
||||
return "Unknown PCRE error ($error)";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches the condition check to the appropriate matching method.
|
||||
*
|
||||
* @param string $condition The name of the condition to check (e.g., 'ip', 'body', 'flood-count').
|
||||
* @param mixed $value The value associated with the condition in the filter config.
|
||||
* @param array<string, mixed> $post The post data array.
|
||||
* @param array<int, array<string, mixed>> $floodEntries The recent post history.
|
||||
* @param array<string, mixed> $allConditions All conditions for the current filter (needed for dependencies like flood-count/flood-time).
|
||||
* @return bool True if the condition matches, false otherwise.
|
||||
* @throws \InvalidArgumentException If regex is invalid.
|
||||
* @throws \RuntimeException For errors during custom callbacks or flood condition checks.
|
||||
*/
|
||||
private function matchCondition(
|
||||
string $condition,
|
||||
mixed $value,
|
||||
array $post,
|
||||
array $floodEntries,
|
||||
array $allConditions
|
||||
): bool {
|
||||
$conditionLower = \strtolower($condition);
|
||||
|
||||
switch ($conditionLower) {
|
||||
case 'custom':
|
||||
|
||||
return $this->matchCustom($value, $post);
|
||||
case 'flood-match':
|
||||
$this->validateType($value, 'array', 'flood-match condition value');
|
||||
|
||||
return $this->matchFloodCondition($value, $post, $floodEntries);
|
||||
case 'flood-time':
|
||||
$this->validateType($value, 'numeric', 'flood-time condition value');
|
||||
|
||||
return $this->checkFloodTime((int)$value, $floodEntries);
|
||||
case 'flood-count':
|
||||
$this->validateType($value, 'numeric', 'flood-count condition value');
|
||||
$timeLimit = $allConditions['flood-time'] ?? 0;
|
||||
$this->validateType($timeLimit, 'numeric', 'flood-time condition value');
|
||||
|
||||
return $this->checkFloodCount((int)$value, $floodEntries, (int)$timeLimit);
|
||||
case 'name':
|
||||
case 'trip':
|
||||
case 'email':
|
||||
case 'subject':
|
||||
case 'body':
|
||||
case 'filehash':
|
||||
$this->validateType($value, 'string', "'{$conditionLower}' condition pattern");
|
||||
if (!isset($post[$condition]) && $condition !== 'filehash') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->checkRegex($value, $post[$condition] ?? '', $condition);
|
||||
case 'body_reg':
|
||||
$this->validateType($value, 'array', 'body_reg condition patterns');
|
||||
|
||||
return $this->matchBodyReg($value, $post);
|
||||
case 'filename':
|
||||
case 'extension':
|
||||
$this->validateType($value, 'string', "'{$conditionLower}' condition pattern");
|
||||
|
||||
return $this->matchFileCondition($value, $condition, $post);
|
||||
case 'ip':
|
||||
$this->validateType($value, 'string', 'IP condition pattern');
|
||||
|
||||
if (!isset($post['ip'])) {
|
||||
$this->logger->log(LogDriver::WARNING, "Post missing 'ip' field for IP condition");
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->checkRegex($value, $post['ip'], 'IP');
|
||||
case 'rdns':
|
||||
$this->validateType($value, 'string', 'RDNS condition pattern');
|
||||
|
||||
if (!isset($post['ip'])) {
|
||||
$this->logger->log(LogDriver::WARNING, "Post missing 'ip' field for RDNS condition");
|
||||
return false;
|
||||
}
|
||||
|
||||
$hostname = \rDNS($post['ip']);
|
||||
if ($hostname === $post['ip']) {
|
||||
$this->logger->log(LogDriver::WARNING, "RDNS lookup failed for IP: {$post['ip']}");
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->checkRegex($value, $hostname, 'RDNS');
|
||||
case 'agent':
|
||||
$this->validateType($value, 'array', 'Agent condition list');
|
||||
return $this->matchAgentCondition($value);
|
||||
case 'op':
|
||||
case 'has_file':
|
||||
case 'board':
|
||||
case 'password':
|
||||
if (!isset($post[$condition])) {
|
||||
$this->logger->log(LogDriver::WARNING, "Post missing field '{$conditionLower}' required for condition.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return $post[$condition] === $value;
|
||||
default:
|
||||
$this->logger->log(LogDriver::WARNING, "Encountered unknown filter condition type: '{$condition}'.");
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current User-Agent is present in a given list of strings.
|
||||
*
|
||||
* @param array<int, mixed> $value The list of user-agent strings from the filter config.
|
||||
* @return bool True if the current User-Agent is found (strictly) in the list, false otherwise.
|
||||
*/
|
||||
private function matchAgentCondition(array $value): bool {
|
||||
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? null;
|
||||
if ($userAgent === null) {
|
||||
$this->logger->log(
|
||||
LogDriver::WARNING,
|
||||
"HTTP_USER_AGENT server variable not available for 'agent' condition."
|
||||
);
|
||||
}
|
||||
|
||||
$validAgents = \array_filter($value, 'is_string');
|
||||
|
||||
if (\count($validAgents) !== \count($value)) {
|
||||
$this->logger->log(
|
||||
LogDriver::WARNING,
|
||||
"Non-string value found in 'agent' condition list. Only string values will be checked."
|
||||
);
|
||||
}
|
||||
|
||||
return \in_array($userAgent, $validAgents, true);
|
||||
}
|
||||
/**
|
||||
* Executes a custom filter callback provided in the filter configuration.
|
||||
*
|
||||
* @param callable $callback The user-defined filter function (must accept post array, return bool).
|
||||
* @param array<string, mixed> $post The post data array.
|
||||
* @return bool The boolean result returned by the callback.
|
||||
* @throws \RuntimeException Wraps any exception thrown by the callback for context.
|
||||
* @throws \Throwable Original exception is wrapped in RuntimeException
|
||||
*/
|
||||
private function matchCustom(callable $callback, array $post): bool {
|
||||
try {
|
||||
$result = $callback($post);
|
||||
if (!\is_bool($result)) {
|
||||
throw new \UnexpectedValueException("Custom filter callback did not return a boolean value.");
|
||||
}
|
||||
return $result;
|
||||
} catch (\Throwable $e) {
|
||||
$functionName = 'anonymous function';
|
||||
|
||||
if (\is_string($callback)) {
|
||||
$functionName = $callback;
|
||||
} elseif (\is_array($callback) && \count($callback) == 2) {
|
||||
$class = \is_object($callback[0]) ? \get_class($callback[0]) : (string)$callback[0];
|
||||
$method = (string)$callback[1];
|
||||
$separator = \is_object($callback[0]) ? '->' : '::';
|
||||
$functionName = $class . $separator . $method;
|
||||
}
|
||||
|
||||
throw new \RuntimeException(
|
||||
"Error executing custom filter function '{$functionName}': " . $e->getMessage(),
|
||||
0,
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the post body matches any of the provided regex patterns.
|
||||
*
|
||||
* @param array<int, string> $patterns An array of regex patterns.
|
||||
* @param array<string, mixed> $post The post data array, expects 'body_nomarkup'.
|
||||
* @return bool True if any pattern matches the 'body_nomarkup', false otherwise.
|
||||
* @throws \InvalidArgumentException If any pattern is not a string or is an invalid regex.
|
||||
*/
|
||||
private function matchBodyReg(array $patterns, array $post): bool {
|
||||
if (!isset($post['body_nomarkup'])) {
|
||||
$this->logger->log(LogDriver::WARNING, "Post missing 'body_nomarkup' for 'body_reg' condition.");
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($patterns as $index => $pattern) {
|
||||
$this->validateType($pattern, 'string', "body_reg pattern #{$index}");
|
||||
|
||||
if ($this->checkRegex($pattern, $post['body_nomarkup'], "body_reg pattern #{$index}")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if any recent post matches a set of flood conditions relative to the current post.
|
||||
*
|
||||
* @param array<int, mixed> $match An array of conditions to check against each flood entry.
|
||||
* @param array<string, mixed> $post The current post data.
|
||||
* @param array<int, array<string, mixed>> $floodEntries Recent post history.
|
||||
* @return bool True if any flood entry satisfies all specified $match conditions.
|
||||
*/
|
||||
private function matchFloodCondition(array $match, array $post, array $floodEntries): bool {
|
||||
if (empty($match)) {
|
||||
$this->logger->log(LogDriver::WARNING, "Empty condition list provided for 'flood-match'.");
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($floodEntries as $floodPost) {
|
||||
if ($this->matchesAllConditions($match, $floodPost, $post)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a flood post matches all given conditions.
|
||||
*
|
||||
* @param array<int, mixed> $match Conditions to verify.
|
||||
* @param array<string, mixed> $floodPost Previous post to compare against.
|
||||
* @param array<string, mixed> $post Current post data.
|
||||
* @return bool True if all conditions match.
|
||||
* @throws \RuntimeException When condition check fails.
|
||||
*/
|
||||
private function matchesAllConditions(array $match, array $floodPost, array $post): bool {
|
||||
foreach ($match as $condition) {
|
||||
try {
|
||||
if (!$this->floodService->checkFloodCondition($condition, $floodPost, $post)) {
|
||||
return false;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$conditionStr = \is_scalar($condition) ? (string)$condition : \json_encode($condition);
|
||||
throw new \RuntimeException("Error checking flood-match sub-condition '{$conditionStr}': " . $e->getMessage(), 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if any recent post was made within the specified time window.
|
||||
*
|
||||
* @param int $time The time window in seconds.
|
||||
* @param array<int, array<string, mixed>> $floodEntries Recent post history, expecting 'time' key.
|
||||
* @return bool True if any flood entry's timestamp is within the window, false otherwise.
|
||||
*/
|
||||
private function checkFloodTime(int $time, array $floodEntries): bool {
|
||||
if ($time < 0) {
|
||||
$this->logger->log(LogDriver::WARNING, "Negative time value '{$time}' provided for 'flood-time'.");
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($floodEntries as $floodPost) {
|
||||
if (!isset($floodPost['time'])) {
|
||||
$this->logger->log(LogDriver::WARNING, "Flood entry missing 'time' field");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (\time() - $floodPost['time'] <= $time) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the count of recent posts within a time limit meets or exceeds a treshold.
|
||||
*
|
||||
* @param int $threshold The minimum number of posts required.
|
||||
* @param array<int, array<string, mixed>> $floodEntries Recent post history, expecting 'time' key.
|
||||
* @param int $timeLimit The time window (in seconds) to count posts within.
|
||||
* @return bool True if the count meets or exceeds the treshold, false otherwise.
|
||||
*/
|
||||
private function checkFloodCount(int $threshold, array $floodEntries, int $timeLimit): bool {
|
||||
$count = \count(\array_filter(
|
||||
$floodEntries,
|
||||
function ($floodPost) use ($timeLimit) {
|
||||
return isset($floodPost['time']) && \time() - $floodPost['time'] <= $timeLimit;
|
||||
}
|
||||
));
|
||||
|
||||
return $count >= $threshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if any file attached to the post matches a regex pattern on a specific field.
|
||||
*
|
||||
* @param string $value The regex pattern.
|
||||
* @param string $condition The file field to check ('filename', 'extension', 'hash').
|
||||
* @param array<string, mixed> $post The post data array, expects 'files' array.
|
||||
* @return bool True if any file matches the condition, false otherwise.
|
||||
* @throws \InvalidArgumentException If the pattern is an invalid regex.
|
||||
*/
|
||||
private function matchFileCondition(string $value, string $condition, array $post): bool {
|
||||
if (empty($post['files']) || !\is_array($post['files'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($post['files'] as $file) {
|
||||
if (!\is_array($file)) {
|
||||
$this->logger->log(LogDriver::WARNING, "File entry is not an array");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($file[$condition])) {
|
||||
$this->logger->log(LogDriver::WARNING, "File missing '{$condition}' field");
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->checkRegex($value, $file[$condition], "file '{$condition}'")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
133
inc/Service/FloodService.php
Normal file
133
inc/Service/FloodService.php
Normal file
@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace Vichan\Service;
|
||||
|
||||
use Vichan\Data\FloodQueries;
|
||||
|
||||
/**
|
||||
* Manages post flood detection logic based on recent post history.
|
||||
* Uses filter configurations to determine relevant time windows.
|
||||
*/
|
||||
class FloodService {
|
||||
/**
|
||||
* @var FloodQueries Handles database interactions for flood data.
|
||||
*/
|
||||
private FloodQueries $floodQueries;
|
||||
|
||||
/**
|
||||
* @var array<int, array<string, mixed>> Filter configurations.
|
||||
*/
|
||||
private array $filters;
|
||||
|
||||
/**
|
||||
* @var int Cache for the calculated maximum flood time (-1 means not calculated).
|
||||
*/
|
||||
private int $flood_cache;
|
||||
|
||||
/**
|
||||
* Constructor for FloodService.
|
||||
*
|
||||
* @param FloodQueries $floodQueries The database query handler for flood data.
|
||||
* @param array<int, array<string, mixed>> $filters Filter configurations.
|
||||
* @param int $flood_cache Optional pre-calculated max flood time.
|
||||
*/
|
||||
public function __construct(FloodQueries $floodQueries, array $filters, int $flood_cache = -1) {
|
||||
$this->floodQueries = $floodQueries;
|
||||
$this->filters = $filters;
|
||||
$this->flood_cache = $flood_cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates or retrieves the maximum flood time window from filters.
|
||||
*
|
||||
* @return int The maximum 'flood-time' value found in filters, or 0 if none.
|
||||
*/
|
||||
public function getMaxFloodTime(): int {
|
||||
if (isset($this->flood_cache) && $this->flood_cache !== -1) {
|
||||
return $this->flood_cache;
|
||||
}
|
||||
|
||||
$maxTime = 0;
|
||||
foreach ($this->filters as $filter) {
|
||||
if (isset($filter['condition']['flood-time']) && $filter['condition']['flood-time'] > $maxTime) {
|
||||
$maxTime = $filter['condition']['flood-time'];
|
||||
}
|
||||
}
|
||||
$this->flood_cache = $maxTime;
|
||||
|
||||
return $maxTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes flood entries older than the maximum relevant flood time.
|
||||
*/
|
||||
public function purgeOldEntries(): void {
|
||||
$maxTime = $this->getMaxFloodTime();
|
||||
|
||||
if ($maxTime > 0) {
|
||||
$this->floodQueries->purgeOldEntries($maxTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves recent flood entries relevant to the given post's IP, body hash, or file hash.
|
||||
*
|
||||
* @param array<string, mixed> $post The post data array. Expects 'ip', 'body_nomarkup', optionally 'filehash', 'has_file'.
|
||||
* @return array<int, array<string, mixed>> List of matching flood entries.
|
||||
*/
|
||||
public function getFloodEntries(array $post): array {
|
||||
$bodyHash = \make_comment_hex($post['body_nomarkup'] ?? '');
|
||||
$fileHash = isset($post['has_file']) && $post['has_file'] ? $post['filehash'] : null;
|
||||
|
||||
return $this->floodQueries->getRecentFloodEntries($post['ip'], $bodyHash, $fileHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares a specific field between a historical flood entry and the current post.
|
||||
*
|
||||
* Valid conditions: 'ip', 'body', 'file', 'board', 'isreply'.
|
||||
*
|
||||
* @param string $condition The condition to check.
|
||||
* @param array<string, mixed> $floodPost A single historical flood entry.
|
||||
* @param array<string, mixed> $post The current post data.
|
||||
* @return bool True if the condition matches between the two posts.
|
||||
* @throws \InvalidArgumentException If the condition string is not recognized.
|
||||
*/
|
||||
public function checkFloodCondition(string $condition, array $floodPost, array $post): bool {
|
||||
switch ($condition) {
|
||||
case 'ip':
|
||||
return isset($floodPost['ip'], $post['ip']) && $floodPost['ip'] === $post['ip'];
|
||||
case 'body':
|
||||
$currentBodyHash = \make_comment_hex($post['body_nomarkup'] ?? '');
|
||||
return isset($floodPost['posthash']) && $floodPost['posthash'] === $currentBodyHash;
|
||||
case 'file':
|
||||
$hasFile = $post['has_file'] ?? false;
|
||||
$currentFileHash = ($hasFile && isset($post['filehash'])) ? $post['filehash'] : null;
|
||||
return isset($floodPost['filehash']) && $currentFileHash !== null && $floodPost['filehash'] === $currentFileHash;
|
||||
case 'board':
|
||||
return isset($floodPost['board'], $post['board']) && $floodPost['board'] === $post['board'];
|
||||
case 'isreply':
|
||||
$currentIsReply = isset($post['thread']);
|
||||
return isset($floodPost['isreply']) && (bool)$floodPost['isreply'] === $currentIsReply;
|
||||
default:
|
||||
throw new \InvalidArgumentException('Invalid filter flood condition: ' . $condition);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a new entry in the flood table for the given post.
|
||||
*
|
||||
* @param array<string, mixed> $post The post data array. Expects 'ip', 'body_nomarkup', 'board', 'thread' optionally 'filehash', 'has_file'.
|
||||
*/
|
||||
public function recordFloodEntry(array $post): void {
|
||||
$this->floodQueries->addFloodEntry(
|
||||
$post['ip'],
|
||||
\make_comment_hex($post['body_nomarkup'] ?? ''),
|
||||
isset($post['has_file']) && $post['has_file'] ? $post['filehash'] : null,
|
||||
$post['board'],
|
||||
\time(),
|
||||
isset($post['thread'])
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -1,8 +1,11 @@
|
||||
<?php
|
||||
namespace Vichan;
|
||||
|
||||
use Vichan\Controller\FloodManager;
|
||||
use Vichan\Data\Driver\{CacheDriver, HttpDriver, ErrorLogLogDriver, FileLogDriver, LogDriver, StderrLogDriver, SyslogLogDriver};
|
||||
use Vichan\Data\{IpNoteQueries, UserPostQueries, ReportQueries};
|
||||
use Vichan\Data\{FloodQueries, IpNoteQueries, UserPostQueries, ReportQueries};
|
||||
use Vichan\Service\FilterService;
|
||||
use Vichan\Service\FloodService;
|
||||
use Vichan\Service\HCaptchaQuery;
|
||||
use Vichan\Service\SecureImageCaptchaQuery;
|
||||
use Vichan\Service\ReCaptchaQuery;
|
||||
@ -84,7 +87,26 @@ function build_context(array $config): Context {
|
||||
),
|
||||
UserPostQueries::class => fn(Context $c): UserPostQueries => new UserPostQueries(
|
||||
$c->get(\PDO::class)
|
||||
)
|
||||
),
|
||||
FloodQueries::class => fn(Context $c): FloodQueries => new FloodQueries(
|
||||
$c->get(\PDO::class)
|
||||
),
|
||||
FloodService::class => fn(Context $c): FloodService => new FloodService(
|
||||
$c->get(FloodQueries::class),
|
||||
$c->get('config')['filters'],
|
||||
$c->get('config')['flood_cache']
|
||||
),
|
||||
FilterService::class => fn(Context $c): FilterService => new FilterService(
|
||||
$c->get('config')['filters'],
|
||||
$c->get(FloodService::class),
|
||||
$c->get(LogDriver::class)
|
||||
),
|
||||
FloodManager::class => fn(Context $c): FloodManager => new FloodManager(
|
||||
$c->get(FilterService::class),
|
||||
$c->get(FloodService::class),
|
||||
$c->get(IpNoteQueries::class),
|
||||
$c->get(LogDriver::class)
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
|
286
inc/filters.php
286
inc/filters.php
@ -5,271 +5,41 @@
|
||||
*/
|
||||
|
||||
use Vichan\Context;
|
||||
use Vichan\Data\IpNoteQueries;
|
||||
use Vichan\Controller\FloodManager;
|
||||
|
||||
defined('TINYBOARD') or exit;
|
||||
|
||||
class Filter {
|
||||
public $flood_check;
|
||||
private $condition;
|
||||
private $post;
|
||||
function do_filters(Context $ctx, array $post): void {
|
||||
$config = $ctx->get('config');
|
||||
|
||||
|
||||
public function __construct(array $arr) {
|
||||
foreach ($arr as $key => $value) {
|
||||
$this->$key = $value;
|
||||
}
|
||||
}
|
||||
|
||||
public function match($condition, $match) {
|
||||
$condition = strtolower($condition);
|
||||
$post = &$this->post;
|
||||
|
||||
switch($condition) {
|
||||
case 'custom':
|
||||
if (!is_callable($match)) {
|
||||
error('Custom condition for filter is not callable!');
|
||||
}
|
||||
return $match($post);
|
||||
case 'flood-match':
|
||||
if (!is_array($match)) {
|
||||
error('Filter condition "flood-match" must be an array.');
|
||||
}
|
||||
|
||||
// Filter out "flood" table entries which do not match this filter.
|
||||
|
||||
$flood_check_matched = array();
|
||||
|
||||
foreach ($this->flood_check as $flood_post) {
|
||||
foreach ($match as $flood_match_arg) {
|
||||
switch ($flood_match_arg) {
|
||||
case 'ip':
|
||||
if ($flood_post['ip'] != $_SERVER['REMOTE_ADDR']) {
|
||||
continue 3;
|
||||
}
|
||||
break;
|
||||
case 'body':
|
||||
if ($flood_post['posthash'] != make_comment_hex($post['body_nomarkup'])) {
|
||||
continue 3;
|
||||
}
|
||||
break;
|
||||
case 'file':
|
||||
if (!isset($post['filehash'])) {
|
||||
return false;
|
||||
}
|
||||
if ($flood_post['filehash'] != $post['filehash']) {
|
||||
continue 3;
|
||||
}
|
||||
break;
|
||||
case 'board':
|
||||
if ($flood_post['board'] != $post['board']) {
|
||||
continue 3;
|
||||
}
|
||||
break;
|
||||
case 'isreply':
|
||||
if ($flood_post['isreply'] == $post['op']) {
|
||||
continue 3;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
error('Invalid filter flood condition: ' . $flood_match_arg);
|
||||
}
|
||||
}
|
||||
$flood_check_matched[] = $flood_post;
|
||||
}
|
||||
|
||||
$this->flood_check = $flood_check_matched;
|
||||
return !empty($this->flood_check);
|
||||
case 'flood-time':
|
||||
foreach ($this->flood_check as $flood_post) {
|
||||
if (time() - $flood_post['time'] <= $match) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
case 'flood-count':
|
||||
$count = 0;
|
||||
foreach ($this->flood_check as $flood_post) {
|
||||
if (time() - $flood_post['time'] <= $this->condition['flood-time']) {
|
||||
++$count;
|
||||
}
|
||||
}
|
||||
return $count >= $match;
|
||||
case 'name':
|
||||
return preg_match($match, $post['name']);
|
||||
case 'trip':
|
||||
return $match === $post['trip'];
|
||||
case 'email':
|
||||
return preg_match($match, $post['email']);
|
||||
case 'subject':
|
||||
return preg_match($match, $post['subject']);
|
||||
case 'body':
|
||||
return preg_match($match, $post['body_nomarkup']);
|
||||
case 'filehash':
|
||||
return $match === $post['filehash'];
|
||||
case 'filename':
|
||||
if (!$post['files']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($post['files'] as $file) {
|
||||
if (preg_match($match, $file['filename'])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
case 'extension':
|
||||
if (!$post['files']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($post['files'] as $file) {
|
||||
if (preg_match($match, $file['extension'])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
case 'ip':
|
||||
return preg_match($match, $_SERVER['REMOTE_ADDR']);
|
||||
case 'op':
|
||||
return $post['op'] == $match;
|
||||
case 'has_file':
|
||||
return $post['has_file'] == $match;
|
||||
case 'board':
|
||||
return $post['board'] == $match;
|
||||
case 'password':
|
||||
return $post['password'] == $match;
|
||||
case 'unshorten':
|
||||
$extracted_urls = get_urls($post['body_nomarkup']);
|
||||
foreach ($extracted_urls as $url) {
|
||||
if (preg_match($match, trace_url($url))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
default:
|
||||
error('Unknown filter condition: ' . $condition);
|
||||
}
|
||||
}
|
||||
|
||||
public function action(Context $ctx) {
|
||||
global $board;
|
||||
|
||||
$this->add_note = isset($this->add_note) ? $this->add_note : false;
|
||||
if ($this->add_note) {
|
||||
$note_queries = $ctx->get(IpNoteQueries::class);
|
||||
$note_queries->add($_SERVER['REMOTE_ADDR'], -1, 'Autoban message: ' . $this->post['body']);
|
||||
}
|
||||
if (isset($this->action)) {
|
||||
switch($this->action) {
|
||||
case 'reject':
|
||||
error(isset($this->message) ? $this->message : 'Posting throttled by filter.');
|
||||
case 'ban':
|
||||
if (!isset($this->reason)) {
|
||||
error('The ban action requires a reason.');
|
||||
}
|
||||
|
||||
$this->expires = isset($this->expires) ? $this->expires : false;
|
||||
$this->reject = isset($this->reject) ? $this->reject : true;
|
||||
$this->all_boards = isset($this->all_boards) ? $this->all_boards : false;
|
||||
|
||||
Bans::new_ban($_SERVER['REMOTE_ADDR'], $this->reason, $this->expires, $this->all_boards ? false : $board['uri'], -1);
|
||||
|
||||
if ($this->reject) {
|
||||
if (isset($this->message)) {
|
||||
error($message);
|
||||
}
|
||||
|
||||
checkBan($board['uri']);
|
||||
exit;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
error('Unknown filter action: ' . $this->action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function check(array $post) {
|
||||
$this->post = $post;
|
||||
foreach ($this->condition as $condition => $value) {
|
||||
if ($condition[0] == '!') {
|
||||
$NOT = true;
|
||||
$condition = substr($condition, 1);
|
||||
} else {
|
||||
$NOT = false;
|
||||
}
|
||||
|
||||
if ($this->match($condition, $value) == $NOT) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function purge_flood_table() {
|
||||
global $config;
|
||||
|
||||
// Determine how long we need to keep a cache of posts for flood prevention. Unfortunately, it is not
|
||||
// aware of flood filters in other board configurations. You can solve this problem by settings the
|
||||
// config variable $config['flood_cache'] (seconds).
|
||||
|
||||
if ($config['flood_cache'] != -1) {
|
||||
$max_time = &$config['flood_cache'];
|
||||
} else {
|
||||
$max_time = 0;
|
||||
foreach ($config['filters'] as $filter) {
|
||||
if (isset($filter['condition']['flood-time'])) {
|
||||
$max_time = max($max_time, $filter['condition']['flood-time']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$time = time() - $max_time;
|
||||
|
||||
query("DELETE FROM ``flood`` WHERE `time` < $time") or error(db_error());
|
||||
}
|
||||
|
||||
function do_filters(Context $ctx, array $post) {
|
||||
global $config;
|
||||
|
||||
if (!isset($config['filters']) || empty($config['filters'])) {
|
||||
if (empty($config['filters'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($config['filters'] as $filter) {
|
||||
if (isset($filter['condition']['flood-match'])) {
|
||||
$has_flood = true;
|
||||
break;
|
||||
$floodManager = $ctx->get(FloodManager::class);
|
||||
$filterResult = $floodManager->processPost($post);
|
||||
|
||||
if ($filterResult) {
|
||||
$action = $filterResult['action'] ?? 'reject';
|
||||
|
||||
if ($action === 'reject') {
|
||||
error($filterResult['message'] ?? _('Posting throttled by filter.'));
|
||||
} elseif ($action === 'ban') {
|
||||
Bans::new_ban(
|
||||
$post['ip'],
|
||||
$filterResult['reason'] ?? _('Banned by filter'),
|
||||
$filterResult['expires'] ?? false,
|
||||
$filterResult['all_boards'] ? false : $post['board'],
|
||||
-1
|
||||
);
|
||||
|
||||
if ($filterResult['reject'] ?? true) {
|
||||
error(
|
||||
$filterResult['message']
|
||||
??
|
||||
_('You have been banned. <a href="/banned.php">Click here to view.</a>')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($has_flood)) {
|
||||
if ($post['has_file']) {
|
||||
$query = prepare("SELECT * FROM ``flood`` WHERE `ip` = :ip OR `posthash` = :posthash OR `filehash` = :filehash");
|
||||
$query->bindValue(':ip', $_SERVER['REMOTE_ADDR']);
|
||||
$query->bindValue(':posthash', make_comment_hex($post['body_nomarkup']));
|
||||
$query->bindValue(':filehash', $post['filehash']);
|
||||
} else {
|
||||
$query = prepare("SELECT * FROM ``flood`` WHERE `ip` = :ip OR `posthash` = :posthash");
|
||||
$query->bindValue(':ip', $_SERVER['REMOTE_ADDR']);
|
||||
$query->bindValue(':posthash', make_comment_hex($post['body_nomarkup']));
|
||||
}
|
||||
$query->execute() or error(db_error($query));
|
||||
$flood_check = $query->fetchAll(PDO::FETCH_ASSOC);
|
||||
} else {
|
||||
$flood_check = false;
|
||||
}
|
||||
|
||||
foreach ($config['filters'] as $filter_array) {
|
||||
$filter = new Filter($filter_array);
|
||||
$filter->flood_check = $flood_check;
|
||||
if ($filter->check($post)) {
|
||||
$filter->action($ctx);
|
||||
}
|
||||
}
|
||||
|
||||
purge_flood_table();
|
||||
}
|
||||
|
@ -867,22 +867,6 @@ function threadExists($id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function insertFloodPost(array $post) {
|
||||
global $board;
|
||||
|
||||
$query = prepare("INSERT INTO ``flood`` VALUES (NULL, :ip, :board, :time, :posthash, :filehash, :isreply)");
|
||||
$query->bindValue(':ip', $_SERVER['REMOTE_ADDR']);
|
||||
$query->bindValue(':board', $board['uri']);
|
||||
$query->bindValue(':time', time());
|
||||
$query->bindValue(':posthash', make_comment_hex($post['body_nomarkup']));
|
||||
if ($post['has_file'])
|
||||
$query->bindValue(':filehash', $post['filehash']);
|
||||
else
|
||||
$query->bindValue(':filehash', null, PDO::PARAM_NULL);
|
||||
$query->bindValue(':isreply', !$post['op'], PDO::PARAM_INT);
|
||||
$query->execute() or error(db_error($query));
|
||||
}
|
||||
|
||||
function post(array $post) {
|
||||
global $pdo, $board;
|
||||
$query = prepare(sprintf("INSERT INTO ``posts_%s`` VALUES ( NULL, :thread, :subject, :email, :name, :trip, :capcode, :body, :body_nomarkup, :time, :time, :files, :num_files, :filehash, :password, :ip, :sticky, :locked, :cycle, 0, :embed, :slug)", $board['uri']));
|
||||
|
3
post.php
3
post.php
@ -597,6 +597,7 @@ if (isset($_POST['delete'])) {
|
||||
// Check if banned
|
||||
checkBan($board['uri']);
|
||||
|
||||
$post['ip'] = $_SERVER['REMOTE_ADDR'];
|
||||
// Check for CAPTCHA right after opening the board so the "return" link is in there.
|
||||
try {
|
||||
$provider = $config['captcha']['provider'];
|
||||
@ -1272,8 +1273,6 @@ if (isset($_POST['delete'])) {
|
||||
nntp_publish($message, $msgid);
|
||||
}
|
||||
|
||||
insertFloodPost($post);
|
||||
|
||||
// Handle cyclical threads
|
||||
if (!$post['op'] && isset($thread['cycle']) && $thread['cycle']) {
|
||||
delete_cyclical_posts($board['uri'], $post['thread'], $config['cycle_limit']);
|
||||
|
Loading…
x
Reference in New Issue
Block a user