- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
175 lines
5.2 KiB
PHP
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'
|
|
);
|
|
}
|
|
}
|