Files
michaelschiemer/src/Framework/Http/Middlewares/ServeStaticFilesMiddleware.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

175 lines
5.2 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\Http\Middlewares;
use App\Framework\Core\PathProvider;
use App\Framework\DateTime\Clock;
use App\Framework\Filesystem\Storage;
use App\Framework\Http\Headers;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\HttpResponse;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\Next;
use App\Framework\Http\RequestStateManager;
use App\Framework\Http\Status;
class ServeStaticFilesMiddleware implements HttpMiddleware
{
/** @var array<string> */
private array $allowedExtensions = [
'jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'svg',
'css', 'js', 'woff', 'woff2', 'ttf', 'eot',
'pdf', 'ico', 'xml', 'json',
];
/** @var array<string, string> */
private array $mimeTypes = [
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'webp' => 'image/webp',
'avif' => 'image/avif',
'svg' => 'image/svg+xml',
'css' => 'text/css',
'js' => 'application/javascript',
'woff' => 'font/woff',
'woff2' => 'font/woff2',
'ttf' => 'font/ttf',
'eot' => 'application/vnd.ms-fontobject',
'pdf' => 'application/pdf',
'ico' => 'image/x-icon',
'xml' => 'application/xml',
'json' => 'application/json',
];
public function __construct(
private readonly PathProvider $pathProvider,
private readonly Clock $clock,
private readonly Storage $storage,
private readonly string $mediaPrefix = '/media'
) {
}
public function __invoke(MiddlewareContext $context, Next $next, RequestStateManager $stateManager): MiddlewareContext
{
$request = $context->request;
$path = $request->path;
// Prüfen ob es sich um eine Media-Anfrage handelt
if (str_starts_with($path, $this->mediaPrefix)) {
$filePath = substr($path, strlen($this->mediaPrefix));
$response = $this->serveStaticFile($filePath);
return new MiddlewareContext($request, $response);
}
return $next($context);
}
private function serveStaticFile(string $filePath): HttpResponse
{
// Pfad bereinigen (verhindert Directory Traversal Angriffe)
$filePath = $this->sanitizePath($filePath);
// Dateiendung prüfen
$extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
if (! in_array($extension, $this->allowedExtensions)) {
return $this->notFound();
}
// Vollständigen Dateipfad konstruieren
$fullPath = $this->pathProvider->resolvePath('storage' . $filePath);
// Prüfen ob Datei existiert
if (! $this->storage->exists($fullPath)) {
return $this->notFound();
}
// MIME-Typ ermitteln
$mimeType = $this->mimeTypes[$extension] ?? 'application/octet-stream';
// Storage verwenden
$content = $this->storage->get($fullPath);
// Cache-Control Header basierend auf Dateityp setzen
$cacheControl = $this->getCacheControlHeader($extension);
// ETag für Caching generieren
$etag = '"' . md5($content) . '"';
// Last-Modified Header mit Clock
$lastModified = $this->clock->now()
->format('D, d M Y H:i:s') . ' GMT';
// Headers zusammenstellen
$headers = new Headers([
'Content-Type' => $mimeType,
'Content-Length' => (string) strlen($content),
'Cache-Control' => $cacheControl,
'ETag' => $etag,
'Last-Modified' => $lastModified,
]);
return new HttpResponse(
Status::OK,
$headers,
$content
);
}
private function sanitizePath(string $path): string
{
// Doppelte Slashes entfernen
$path = preg_replace('#/+#', '/', $path) ?? $path;
// Führende/nachfolgende Slashes entfernen
$path = trim($path, '/');
// Sicherstellen, dass kein '..' enthalten ist (verhindert Directory Traversal)
$parts = [];
foreach (explode('/', $path) as $part) {
if ($part === '.') {
continue;
}
if ($part === '..') {
array_pop($parts);
} else {
$parts[] = $part;
}
}
return '/' . implode('/', $parts);
}
private function getCacheControlHeader(string $extension): string
{
// Bilder und Fonts können länger gecacht werden
$imageFontExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'svg', 'woff', 'woff2', 'ttf', 'eot'];
if (in_array($extension, $imageFontExtensions)) {
return 'public, max-age=31536000'; // 1 Jahr
}
// CSS und JS
if (in_array($extension, ['css', 'js'])) {
return 'public, max-age=604800'; // 1 Woche
}
// Sonstige Dateien
return 'public, max-age=86400'; // 1 Tag
}
private function notFound(): HttpResponse
{
return new HttpResponse(
Status::NOT_FOUND,
new Headers(['Content-Type' => 'text/plain']),
'Datei nicht gefunden'
);
}
}