<?php namespace App\Security;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\RateLimiter\RateLimiterFactory;
/**
* Anti-spam checks shared by the public contact/order forms
* (DefaultController::contactAction/orderAction), both built on ContactType.
*/
class SpamGuard
{
// A real visitor needs at least this long to read the form and type a
// message; bots that submit the instant the page loads don't.
private const MIN_FILL_SECONDS = 3;
private RateLimiterFactory $limiter;
public function __construct(RateLimiterFactory $contactFormLimiter)
{
$this->limiter = $contactFormLimiter;
}
/**
* Call when the form is rendered (not submitted), so the next
* submission's fill time can be measured against it.
*/
public function markRendered(Request $request, string $sessionKey): void
{
$request->getSession()->set($sessionKey, time());
}
/**
* True if this submission looks automated: either the honeypot field
* (invisible to real visitors, see ContactType) got filled in, or it
* arrived faster than a human could plausibly have typed a message —
* including never having loaded the form at all, which is how a bot
* that POSTs directly without a prior GET shows up here.
*/
public function isSpam(Request $request, FormInterface $form, string $sessionKey): bool
{
if ($form->has('website') && $form->get('website')->getData()) {
return true;
}
$renderedAt = $request->getSession()->get($sessionKey);
return $renderedAt === null || (time() - $renderedAt) < self::MIN_FILL_SECONDS;
}
/**
* Caps how many times one IP can submit within the configured window
* (see framework.yaml's rate_limiter.contact_form), independently of
* isSpam() above — catches a slower or JS-capable bot (or a determined
* human) that passes both of those checks.
*/
public function isRateLimited(Request $request): bool
{
return !$this->limiter->create($request->getClientIp())->consume(1)->isAccepted();
}
}