CluPilotCloud/app/Http/Controllers/PublicFileController.php

69 lines
2.8 KiB
PHP

<?php
namespace App\Http\Controllers;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
/**
* Dateien, die jeder herunterladen darf: AGB, Auftragsverarbeitungsvertrag,
* technische und organisatorische Maßnahmen, Preisblätter.
*
* Sie liegen in `storage/app/files/public/` und werden ausgeliefert, wie sie
* sind. Kein Verwaltungsbereich, keine Datenbanktabelle: eine Datei ablegen
* genügt, und was nicht dort liegt, gibt es nicht.
*
* ---------------------------------------------------------------------------
* Warum eine eigene Adresse
* ---------------------------------------------------------------------------
*
* Diese Dateien stehen in Verträgen und auf Rechnungen. Eine Adresse, die beim
* nächsten Umbau des Portals wandert, macht aus jedem dieser Verweise einen
* toten Link — und zwar rückwirkend, in Dokumenten, die längst verschickt sind.
* Ein eigener Hostname ist die einzige Zusage, die sich einhalten lässt.
*
* ---------------------------------------------------------------------------
* Versionierte Dateinamen
* ---------------------------------------------------------------------------
*
* `agb-2026-01.pdf`, nicht `agb.pdf`. Eine Fassung, auf die ein Vertrag
* verweist, darf sich unter derselben Adresse NIE ändern — sonst zeigt der
* Verweis in einem Vertrag vom Januar plötzlich auf Bedingungen vom Juli, und
* niemand kann mehr sagen, was vereinbart war. Neue Fassung heißt neue Datei.
*
* Erzwungen wird das hier nicht — eine Regel, die eine Datei mit dem falschen
* Namen abweist, hilft niemandem, der eine hochladen will. Sie steht hier, weil
* dies die Stelle ist, an der jemand nachsieht.
*/
class PublicFileController extends Controller
{
private const ROOT = 'app/files/public';
public function __invoke(string $file): BinaryFileResponse
{
// NUR der Dateiname, ohne Pfad. `basename()` macht aus
// `../../.env` ein `.env`, und das liegt in diesem Verzeichnis nicht —
// damit ist der Pfad-Ausbruch beantwortet, bevor er anfängt. Ein
// Verzeichnisbaum unter `public/` gibt es absichtlich nicht: er wäre
// die zweite Gelegenheit für denselben Fehler.
$name = basename($file);
// Ein Punktdatei-Name kommt hier nicht durch. Sie sind in diesem
// Verzeichnis nicht vorgesehen, und ihr Vorkommen wäre ein Versehen.
if ($name === '' || str_starts_with($name, '.')) {
abort(404);
}
$path = storage_path(self::ROOT.'/'.$name);
if (! is_file($path)) {
abort(404);
}
// Inline, nicht als Anhang: eine AGB, die der Browser anzeigt, wird
// gelesen; eine, die er herunterlädt, wird weggeklickt.
return response()->file($path, [
'Content-Disposition' => 'inline; filename="'.$name.'"',
]);
}
}