<?php
/**
 * Csrf — one shared CSRF token get/verify implementation, replacing the
 * ~6 independently hand-rolled `bin2hex(random_bytes(32))` /
 * `hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])` copies that
 * already existed (sign-in.php, sign-up.php, forgot-password.php,
 * reset-password.php, settings/update.php's 'security' section) before
 * this file — those are untouched (already correct, already tested), this
 * is what closes the gap on every OTHER state-changing endpoint.
 *
 * Two ways a request can carry its token, covering both real patterns this
 * app uses:
 *   - Classic <form method="POST">: a hidden `csrf_token` field.
 *   - JSON fetch(): an `X-CSRF-Token` request header (assets/js/csrf.js
 *     adds this automatically to every same-origin fetch() call, so
 *     individual endpoints/pages never had to be touched one by one).
 * Reading from a header rather than the JSON body specifically avoids the
 * "can php://input be read twice" question entirely — every endpoint's own
 * `json_decode(file_get_contents('php://input'), true)` call is completely
 * unaffected by this file ever having looked at the request.
 */
class Csrf
{
    /**
     * Returns the current session's CSRF token, generating one if this is
     * the first time it's needed this session. Safe to call from any page
     * that has already started a session (every real entry point in this
     * app does, via config.php + index.php or its own session_start()).
     */
    public static function token(): string
    {
        if (empty($_SESSION['csrf_token'])) {
            $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
        }
        return $_SESSION['csrf_token'];
    }

    /**
     * True if the request carries a token matching the current session's,
     * checked via hash_equals (timing-safe). Never throws, never exits —
     * callers decide what to do with a false result.
     */
    public static function verify(): bool
    {
        if (empty($_SESSION['csrf_token'])) {
            return false;
        }
        $submitted = $_SERVER['HTTP_X_CSRF_TOKEN']
            ?? $_POST['csrf_token']
            ?? '';
        if ($submitted === '') {
            return false;
        }
        return hash_equals($_SESSION['csrf_token'], (string)$submitted);
    }

    /**
     * Renders the <meta> tag assets/js/csrf.js reads to populate the
     * X-CSRF-Token header on every fetch() call. Call once per page,
     * anywhere in the markup (sidebar.php is the one file included by
     * nearly every authenticated page, so that's where this is actually
     * called from).
     */
    public static function metaTag(): string
    {
        return '<meta name="csrf-token" content="' . htmlspecialchars(self::token(), ENT_QUOTES) . '">';
    }

    /**
     * Hidden form field for classic <form method="POST"> submissions.
     */
    public static function field(): string
    {
        return '<input type="hidden" name="csrf_token" value="' . htmlspecialchars(self::token(), ENT_QUOTES) . '">';
    }
}
