<?php
namespace App\EventSubscriber;
use App\Entity\BlogPost;
use App\Entity\User;
use App\Repository\BlogPostCategoryRepository;
use App\Repository\BlogPostRepository;
use App\Security\AppAuthenticator;
use App\Service\SiteConfig;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
class LoginTargetPathSubscriber implements EventSubscriberInterface
{
use TargetPathTrait;
private const FIREWALL_MAIN = 'main';
private const RUBRIQUE_ROUTES = ['podcast', 'categorie'];
public function __construct(
private Security $security,
private SiteConfig $siteConfig,
private UrlGeneratorInterface $urlGenerator,
private UrlMatcherInterface $urlMatcher,
private BlogPostRepository $blogPostRepository,
private BlogPostCategoryRepository $blogPostCategoryRepository,
private AuthorizationCheckerInterface $authorizationChecker,
) {
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => 'onKernelRequest',
];
}
public function onKernelRequest(RequestEvent $event): void
{
if (!$event->isMainRequest()) {
return;
}
$request = $event->getRequest();
$routeName = $request->attributes->get('_route');
if (
in_array($routeName, self::RUBRIQUE_ROUTES, true)
&& $request->getMethod() === 'GET'
&& null === $this->security->getUser()
&& !$this->siteConfig->isPublicFrontOffice()
&& $request->hasSession()
) {
$targetPath = $request->getRequestUri();
$prepareUrl = $this->urlGenerator->generate('app_prepare_sso', [
'redirect' => $targetPath,
]);
$event->setResponse(new RedirectResponse($prepareUrl));
return;
}
// Cas : utilisateur connecté qui accède à une rubrique — changer d'instance si nécessaire.
if (
in_array($routeName, self::RUBRIQUE_ROUTES, true)
&& $request->getMethod() === 'GET'
&& ($connectedUser = $this->security->getUser()) instanceof User
&& $request->hasSession()
) {
$this->switchInstanceIfNeeded($request, $connectedUser);
// Ne pas rediriger : le contrôleur prend la main normalement.
}
if ($routeName !== AppAuthenticator::LOGIN_ROUTE) {
return;
}
if ($request->getMethod() !== 'GET') {
return;
}
if (null !== $this->security->getUser()) {
return;
}
if (!$request->hasSession()) {
return;
}
$session = $request->getSession();
if ($this->getTargetPath($session, self::FIREWALL_MAIN)) {
return;
}
$referer = $request->headers->get('referer');
if (!$referer) {
return;
}
$targetPath = $this->extractInternalPathFromReferer($referer, $request->getHost());
if (null === $targetPath) {
return;
}
if (!$this->isRubriquePath($targetPath)) {
return;
}
$loginPath = $this->urlGenerator->generate(AppAuthenticator::LOGIN_ROUTE);
$logoutPath = $this->urlGenerator->generate('app_logout');
if (str_starts_with($targetPath, $loginPath) || str_starts_with($targetPath, $logoutPath)) {
return;
}
$this->saveTargetPath($session, self::FIREWALL_MAIN, $targetPath);
}
private function extractInternalPathFromReferer(string $referer, string $currentHost): ?string
{
$parts = parse_url($referer);
if (false === $parts) {
return null;
}
$host = $parts['host'] ?? null;
if ($host !== null && strcasecmp($host, $currentHost) !== 0) {
return null;
}
$path = $parts['path'] ?? null;
if (null === $path || $path === '') {
return null;
}
$query = isset($parts['query']) ? '?' . $parts['query'] : '';
return $path . $query;
}
/**
* Si l'instance courante en session ne donne pas accès au contenu cible,
* bascule automatiquement vers la première instance accessible de l'utilisateur.
*/
private function switchInstanceIfNeeded(Request $request, User $user): void
{
$path = $request->getPathInfo();
try {
$matchedRoute = $this->urlMatcher->match($path);
} catch (\Throwable) {
return;
}
$routeName = $matchedRoute['_route'] ?? null;
$slug = $matchedRoute['slug'] ?? null;
if (!$routeName || !$slug) {
return;
}
$session = $request->getSession();
$currentInstanceId = $session->get('selected_instance_id');
$accessibleInstances = $this->resolveAccessibleInstances($routeName, $slug, $user);
if (empty($accessibleInstances)) {
return;
}
// Si l'instance courante est déjà valide, pas de changement.
foreach ($accessibleInstances as $instance) {
if ($instance->getId() === $currentInstanceId) {
return;
}
}
// Basculer vers la première instance accessible.
$session->set('selected_instance_id', $accessibleInstances[0]->getId());
}
/**
* Retourne les instances activées auxquelles l'utilisateur a accès pour ce contenu.
*
* @return array<int, \App\Entity\Instance>
*/
private function resolveAccessibleInstances(string $routeName, string $slug, User $user): array
{
$instances = [];
if ($routeName === 'categorie') {
$category = $this->blogPostCategoryRepository->findOneBy(['slug' => $slug]);
if (null === $category) {
return [];
}
// Vérifie les critères métier via le voter VIEW_CONTENT.
if (!$this->authorizationChecker->isGranted('VIEW_CONTENT', $category)) {
return [];
}
foreach ($category->getInstances() as $instance) {
if ($instance->isActivated() && $user->getInstances()->contains($instance)) {
$instances[] = $instance;
}
}
return $instances;
}
// podcast
/** @var BlogPost|null $podcast */
$podcast = $this->blogPostRepository->findOneBy(['slug' => $slug]);
if (null === $podcast) {
return [];
}
// Vérifie déjà les critères métier via le voter VIEW_CONTENT.
if (!$this->authorizationChecker->isGranted('VIEW_CONTENT', $podcast)) {
return [];
}
foreach ($podcast->getInstances() as $instance) {
if ($instance->isActivated() && $user->getInstances()->contains($instance)) {
$instances[] = $instance;
}
}
return $instances;
}
private function isRubriquePath(string $targetPath): bool
{
$parsed = parse_url($targetPath);
if (false === $parsed) {
return false;
}
$path = $parsed['path'] ?? null;
if (!$path) {
return false;
}
try {
$matchedRoute = $this->urlMatcher->match($path);
} catch (\Throwable) {
return false;
}
return in_array(($matchedRoute['_route'] ?? null), self::RUBRIQUE_ROUTES, true);
}
}