Files
SkinbaseNova/app/Http/Middleware/ForumRateLimitMiddleware.php

71 lines
2.4 KiB
PHP

<?php
namespace App\Http\Middleware;
use Closure;
use cPad\Plugins\Forum\Services\Security\BotProtectionService;
use Illuminate\Http\Exceptions\ThrottleRequestsException;
use Illuminate\Http\Request;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Symfony\Component\HttpFoundation\Response;
class ForumRateLimitMiddleware
{
public function __construct(
private readonly ThrottleRequests $throttleRequests,
private readonly BotProtectionService $botProtectionService,
) {
}
public function handle(Request $request, Closure $next): Response
{
$routeName = (string) optional($request->route())->getName();
$limiterName = match ($routeName) {
'forum.topic.store' => 'forum-thread-create',
default => 'forum-post-write',
};
try {
return $this->throttleRequests->handle($request, $next, $limiterName);
} catch (ThrottleRequestsException $exception) {
$maxAttempts = (int) ($exception->getHeaders()['X-RateLimit-Limit'] ?? 0);
$this->botProtectionService->recordRateLimitViolation(
$request,
$this->resolveActionName($routeName),
[
'limiter' => $limiterName,
'bucket' => $this->resolveBucket($limiterName, $maxAttempts),
'max_attempts' => $maxAttempts,
'retry_after' => (int) ($exception->getHeaders()['Retry-After'] ?? 0),
'reason' => sprintf('Forum write rate limit exceeded on %s.', $routeName !== '' ? $routeName : 'unknown route'),
],
);
throw $exception;
}
}
private function resolveActionName(string $routeName): string
{
return match ($routeName) {
'forum.topic.store' => 'forum_topic_create',
'forum.post.update' => 'forum_post_update',
default => 'forum_reply_create',
};
}
private function resolveBucket(string $limiterName, int $maxAttempts): string
{
return $maxAttempts <= $this->minuteLimitThreshold($limiterName) ? 'minute' : 'hour';
}
private function minuteLimitThreshold(string $limiterName): int
{
return match ($limiterName) {
'forum-thread-create', 'forum-post-write' => 3,
default => PHP_INT_MAX,
};
}
}