. */ declare(strict_types=1); namespace App\EventSubscriber; use App\Services\System\UpdateExecutor; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\FilterResponseEvent; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; use Twig\Environment; /** * Blocks all web requests when maintenance mode is enabled during updates. */ readonly class MaintenanceModeSubscriber implements EventSubscriberInterface { public function __construct(private UpdateExecutor $updateExecutor, private Environment $twig) { } public static function getSubscribedEvents(): array { return [ // High priority to run before other listeners KernelEvents::REQUEST => ['onKernelRequest', 512], //High priority to run before other listeners KernelEvents::RESPONSE => ['onKernelResponse', -512] // Low priority to run after other listeners ]; } public function onKernelRequest(RequestEvent $event): void { // Only handle main requests if (!$event->isMainRequest()) { return; } // Skip if not in maintenance mode if (!$this->updateExecutor->isMaintenanceMode()) { return; } // Allow CLI requests if (PHP_SAPI === 'cli') { return; } // Get maintenance info $maintenanceInfo = $this->updateExecutor->getMaintenanceInfo(); $lockInfo = $this->updateExecutor->getLockInfo(); // Calculate how long the update has been running $duration = null; if ($lockInfo && isset($lockInfo['started_at'])) { try { $startedAt = new \DateTime($lockInfo['started_at']); $now = new \DateTime(); $duration = $now->getTimestamp() - $startedAt->getTimestamp(); } catch (\Exception) { // Ignore date parsing errors } } // Try to render the Twig template, fall back to simple HTML try { $content = $this->twig->render('maintenance/maintenance.html.twig', [ 'reason' => $maintenanceInfo['reason'] ?? 'Maintenance in progress', 'started_at' => $maintenanceInfo['enabled_at'] ?? null, 'duration' => $duration, ]); } catch (\Exception) { // Fallback to simple HTML if Twig fails $content = $this->getSimpleMaintenanceHtml($maintenanceInfo, $duration); } $response = new Response($content, Response::HTTP_SERVICE_UNAVAILABLE); $response->headers->set('Retry-After', '30'); $response->headers->set('Cache-Control', 'no-store, no-cache, must-revalidate'); $event->setResponse($response); } public function onKernelResponse(ResponseEvent $event) { // Only handle main requests if (!$event->isMainRequest()) { return; } // Skip if not in maintenance mode if (!$this->updateExecutor->isMaintenanceMode()) { return; } // Allow CLI requests if (PHP_SAPI === 'cli') { return; } //Remove all Content-Security-Policy headers to allow loading resources during maintenance $response = $event->getResponse(); $response->headers->remove('Content-Security-Policy'); } /** * Generate a simple maintenance page HTML without Twig. */ private function getSimpleMaintenanceHtml(?array $maintenanceInfo, ?int $duration): string { $reason = htmlspecialchars($maintenanceInfo['reason'] ?? 'Update in progress'); $durationText = $duration !== null ? sprintf('%d seconds', $duration) : 'a moment'; return <<
We're making things better. This should only take a moment.
Update running for {$durationText}
This page will automatically refresh every 15 seconds.