<?php

declare(strict_types=1);

/*
 * This file is part of the Ferienpass package.
 *
 * (c) Richard Henkenjohann <richard@ferienpass.online>
 *
 * For more information visit the project website <https://ferienpass.online>
 * or the documentation under <https://docs.ferienpass.online>.
 */

namespace Ferienpass\CmsBundle\Security;

use Contao\CoreBundle\EventListener\SubrequestCacheSubscriber;
use Ferienpass\CmsBundle\Fragment\FragmentReference;
use Ferienpass\CmsBundle\Page\PageBuilderFactory;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface;

class AccessDeniedHandler implements AccessDeniedHandlerInterface
{
    public function __construct(private readonly PageBuilderFactory $pageBuilderFactory)
    {
    }

    public function handle(Request $request, AccessDeniedException $accessDeniedException): ?Response
    {
        $response = $this->pageBuilderFactory->create($request->attributes->get('pageModel'))
            ->addFragment('main', new FragmentReference('ferienpass.fragment.error403'))
            ->getResponse()
        ;

        // 403 pages are shown to anonymous visitors trying to reach a
        // protected URL (/account, /meine-anmeldungen, …). The body is the
        // CMS login form — same for every anonymous visitor, no per-user
        // data — so the role-bucket cache COULD legitimately share it across
        // the anonymous bucket. But we explicitly downgrade to private
        // because:
        //
        //   1. The Symfony security listener forces this response to 403,
        //      which is NOT in Symfony's `Response::isCacheable()` allowlist.
        //      `UserContextAwarePrivateResponseListener` therefore returns
        //      early without applying the Contao downgrade, so the response
        //      flies out with whatever `FrontendTemplate::setCacheHeaders`
        //      baked on (typically `public, s-maxage=1y`). Varnish 7's
        //      built-in cacheability rules don't cache 403 by default, so
        //      empirically this hasn't leaked yet — but it's one VCL tweak
        //      away from doing so.
        //
        //   2. The login form embeds CSRF tokens. Public caching would
        //      hand every anonymous visitor the same token, then every
        //      login attempt would fail with a CSRF mismatch.
        $response->setPrivate();
        // setPrivate() doesn't drop s-maxage; clear it explicitly so the
        // Cache-Control header is unambiguously private. See
        // {@see \Ferienpass\CmsBundle\Controller\AbstractController::markPageAsPrivate}
        // for the longer rationale.
        $response->headers->removeCacheControlDirective('s-maxage');
        $response->headers->addCacheControlDirective('no-store');
        $response->headers->addCacheControlDirective('must-revalidate');
        $response->setMaxAge(0);
        $response->headers->set(SubrequestCacheSubscriber::MERGE_CACHE_HEADER, '1');

        return $response;
    }
}
