Files
michaelschiemer/src/Framework/View/Processors/CsrfTokenProcessor.php
Michael Schiemer 55a330b223 Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
2025-08-11 20:13:26 +02:00

214 lines
6.5 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\View\Processors;
use App\Framework\Template\Processing\StringProcessor;
use App\Framework\View\RenderContext;
/**
* CSRF-Token-Processor für Cache-sichere CSRF-Token-Behandlung
*
* Dieser Processor löst das Problem mit gecachten CSRF-Tokens:
* 1. Beim Caching: Ersetzt CSRF-Tokens mit Platzhaltern
* 2. Beim Rendern: Ersetzt Platzhalter mit frischen Tokens
*/
final class CsrfTokenProcessor implements StringProcessor
{
private const string CSRF_PLACEHOLDER = '__CSRF_TOKEN_PLACEHOLDER__';
private const string META_PLACEHOLDER = '__CSRF_META_PLACEHOLDER__';
private const string FIELD_PLACEHOLDER = '__CSRF_FIELD_PLACEHOLDER__';
public function __construct(
private readonly bool $cacheMode = false,
private readonly bool $debugMode = false
) {
}
public function process(string $html, RenderContext $context): string
{
return $html;
if (! $this->shouldProcessCsrf($context)) {
return $html;
}
if ($this->cacheMode) {
// Cache-Modus: Ersetze echte Tokens mit Platzhaltern
return $this->replaceTokensWithPlaceholders($html, $context);
} else {
// Render-Modus: Ersetze Platzhalter mit echten Tokens
return $this->replacePlaceholdersWithTokens($html, $context);
}
}
/**
* Prüft ob das Template CSRF-Token-Processing benötigt
*/
private function shouldProcessCsrf(RenderContext $context): bool
{
// Check 1: Data enthält CSRF-Token
if (isset($context->data['csrf_token']) || isset($context->data['_token'])) {
return true;
}
// Check 2: Template-Name deutet auf Formulare hin
$formKeywords = ['form', 'contact', 'login', 'register', 'checkout', 'admin'];
foreach ($formKeywords as $keyword) {
if (str_contains($context->template, $keyword)) {
return true;
}
}
return false;
}
/**
* Cache-Modus: Ersetzt echte CSRF-Tokens mit Platzhaltern
*/
private function replaceTokensWithPlaceholders(string $html, RenderContext $context): string
{
$this->debugLog("Converting CSRF tokens to placeholders for caching");
// 1. Ersetze Template-Syntax CSRF-Aufrufe
$html = $this->replaceTemplateCsrfCalls($html);
// 2. Ersetze echte Token-Werte falls vorhanden
if (isset($context->data['csrf_token'])) {
$html = str_replace($context->data['csrf_token'], self::CSRF_PLACEHOLDER, $html);
}
if (isset($context->data['_token'])) {
$html = str_replace($context->data['_token'], self::CSRF_PLACEHOLDER, $html);
}
// 3. Ersetze HTML-Pattern
$html = $this->replaceHtmlCsrfPatterns($html);
return $html;
}
/**
* Render-Modus: Ersetzt Platzhalter mit echten CSRF-Tokens
*/
private function replacePlaceholdersWithTokens(string $html, RenderContext $context): string
{
$this->debugLog("Converting placeholders to fresh CSRF tokens");
$csrfToken = $this->getCsrfToken($context);
// Ersetze alle Platzhalter-Types
$replacements = [
self::CSRF_PLACEHOLDER => $csrfToken,
self::META_PLACEHOLDER => $csrfToken,
self::FIELD_PLACEHOLDER => $this->generateCsrfField($csrfToken),
];
return str_replace(array_keys($replacements), array_values($replacements), $html);
}
/**
* Ersetzt Template-Syntax CSRF-Aufrufe mit Platzhaltern
*/
private function replaceTemplateCsrfCalls(string $html): string
{
$patterns = [
// {{ csrf_token() }} oder {{ csrf_token }}
'/\{\{\s*csrf_token\(\s*\)\s*\}\}/' => self::CSRF_PLACEHOLDER,
'/\{\{\s*csrf_token\s*\}\}/' => self::CSRF_PLACEHOLDER,
// {!! csrf_token() !!}
'/\{!!\s*csrf_token\(\s*\)\s*!!\}/' => self::CSRF_PLACEHOLDER,
// {{ _token }}
'/\{\{\s*_token\s*\}\}/' => self::CSRF_PLACEHOLDER,
// @csrf Blade-Direktive
'/@csrf/' => self::FIELD_PLACEHOLDER,
// csrf_field() Helper
'/\{\{\s*csrf_field\(\s*\)\s*\}\}/' => self::FIELD_PLACEHOLDER,
'{!! csrf_field() !!}' => self::FIELD_PLACEHOLDER,
];
return preg_replace(array_keys($patterns), array_values($patterns), $html);
}
/**
* Ersetzt HTML-CSRF-Patterns mit Platzhaltern
*/
private function replaceHtmlCsrfPatterns(string $html): string
{
// Hidden Input Fields mit CSRF-Token
$html = preg_replace(
'/<input[^>]*name=["\']_token["\'][^>]*value=["\']([^"\']+)["\'][^>]*>/',
'<input type="hidden" name="_token" value="' . self::CSRF_PLACEHOLDER . '">',
$html
);
// Meta Tags mit CSRF-Token
$html = preg_replace(
'/<meta[^>]*name=["\']csrf-token["\'][^>]*content=["\']([^"\']+)["\'][^>]*>/',
'<meta name="csrf-token" content="' . self::META_PLACEHOLDER . '">',
$html
);
return $html;
}
/**
* Holt den aktuellen CSRF-Token
*/
private function getCsrfToken(RenderContext $context): string
{
// Priorität: Context-Daten > Generierter Token
return $context->data['csrf_token'] ??
$context->data['_token'] ??
$this->generateFreshCsrfToken();
}
/**
* Generiert einen neuen CSRF-Token
*/
private function generateFreshCsrfToken(): string
{
// Integration mit deinem CSRF-System
// Das könnte auch ein Service-Call sein
return bin2hex(random_bytes(32));
}
/**
* Generiert ein komplettes CSRF-Hidden-Field
*/
private function generateCsrfField(string $token): string
{
return '<input type="hidden" name="_token" value="' . $token . '">';
}
/**
* Debug-Logging
*/
private function debugLog(string $message): void
{
// Debug logging removed for production
// Use proper logger in production environment
}
/**
* Factory-Methode für Cache-Modus
*/
public static function forCaching(bool $debugMode = false): self
{
return new self(cacheMode: true, debugMode: $debugMode);
}
/**
* Factory-Methode für Render-Modus
*/
public static function forRendering(bool $debugMode = false): self
{
return new self(cacheMode: false, debugMode: $debugMode);
}
}