From 6bb705e5662d4749fa430043e877982a4bfbc895 Mon Sep 17 00:00:00 2001 From: nexxo Date: Tue, 4 Aug 2026 15:24:35 +0200 Subject: [PATCH] Fix-Runde 1: bind-hosts schreibt keine Rueckverweise mehr in die .env apply() ersetzte eine vorhandene Zeile per preg_replace($pattern, $line, ...) - $line kommt von der Befehlszeile, und preg_replace deutet $1/\1/\\ im ERSATZ als Rueckverweis, auch ohne Klammern im Muster. Ein Hostname mit solchen Zeichen wuerde still verstuemmelt in die Datei geschrieben, die jedes Geheimnis der Installation haelt - und EnvFileEditor::isValidLine() kann das nicht fangen, das Ergebnis ist syntaktisch weiter KEY=value. apply() arbeitet jetzt zeilenweise ohne jede Regex im Ersatzpfad. Zusaetzlich: EnvFileEditor::write() wirft InvalidEnvContentException, wenn der neue Inhalt nicht parst - das war ungefangen und zeigte dem Betreiber einen Stapelabzug auf der Zugangsdatendatei statt eines Satzes wie jeder andere Fehlerpfad in diesem Befehl. Neuer Test deckt den Rueckverweis-Fall ab, ueber die bereits vorhandene (leere) SITE_HOST-Zeile - der Pfad, den preg_replace tatsaechlich traf. Co-Authored-By: Claude Opus 5 --- app/Console/Commands/BindHosts.php | 41 +++++++++++++++++++++----- tests/Feature/BindHostsCommandTest.php | 18 +++++++++++ 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/app/Console/Commands/BindHosts.php b/app/Console/Commands/BindHosts.php index f918660..b5b85e9 100644 --- a/app/Console/Commands/BindHosts.php +++ b/app/Console/Commands/BindHosts.php @@ -3,6 +3,7 @@ namespace App\Console\Commands; use App\Services\Env\EnvFileEditor; +use App\Services\Env\InvalidEnvContentException; use Illuminate\Console\Command; /** @@ -95,7 +96,16 @@ class BindHosts extends Command return self::FAILURE; } - $backup = $env->write($this->apply($content, $missing)); + try { + $backup = $env->write($this->apply($content, $missing)); + } catch (InvalidEnvContentException $e) { + // Abgelehnt heißt hier: NICHTS geschrieben, und die Sicherung ist + // erst gar nicht angelegt worden — der Editor prüft vor beidem. + // Der Betreiber soll das als Satz erfahren, nicht als Stapelabzug. + $this->error('Die Datei wurde nicht geschrieben: '.$e->getMessage()); + + return self::FAILURE; + } $this->info('Geschrieben. Die vorherige Fassung liegt unter '.$backup); $this->line('Danach: php artisan config:cache && php artisan route:cache'); @@ -124,6 +134,12 @@ class BindHosts extends Command /** * Vorhandene Zeile ersetzen, sonst anhängen. * + * Zeilenweise und ohne `preg_replace`: der Wert kommt von der Befehlszeile, + * und `preg_replace` deutet `$1`, `\1` und `\\` im ERSATZ als Rückverweise. + * Aus `--site 'www.example.test$1'` würde still `SITE_HOST=www.example.test` + * — und diese Datei hält jedes Geheimnis dieser Installation. Ein leise + * verstümmelter Wert darin ist schlimmer als eine Fehlermeldung. + * * Ersetzen und nicht nur anhängen, weil ein leerer Schlüssel als fehlend * gilt: `APP_HOST=` steht dann schon da, und ein zweites `APP_HOST=…` * darunter wäre eine Datei mit zwei Antworten auf dieselbe Frage. @@ -132,15 +148,24 @@ class BindHosts extends Command */ private function apply(string $content, array $values): string { - foreach ($values as $key => $value) { - $line = $key.'='.$value; - $pattern = '/^'.preg_quote($key, '/').'=.*$/m'; + $lines = preg_split('/\R/', rtrim($content, "\r\n")); - $content = preg_match($pattern, $content) === 1 - ? preg_replace($pattern, $line, $content, 1) - : rtrim($content, "\n")."\n".$line."\n"; + foreach ($values as $key => $value) { + $replaced = false; + + foreach ($lines as $index => $line) { + if (str_starts_with($line, $key.'=')) { + $lines[$index] = $key.'='.$value; + $replaced = true; + break; + } + } + + if (! $replaced) { + $lines[] = $key.'='.$value; + } } - return $content; + return implode("\n", $lines)."\n"; } } diff --git a/tests/Feature/BindHostsCommandTest.php b/tests/Feature/BindHostsCommandTest.php index 95c2d6e..c460c5b 100644 --- a/tests/Feature/BindHostsCommandTest.php +++ b/tests/Feature/BindHostsCommandTest.php @@ -110,3 +110,21 @@ it('sagt es, wenn schon alles steht, statt eine Sicherung anzulegen', function ( expect(glob($this->envPath.'.bak-*'))->toBeEmpty(); }); + +it('schreibt einen Wert wörtlich, auch wenn er wie ein Rückverweis aussieht', function () { + // SITE_HOST steht schon leer da — genau das, was .env.example wirklich + // ausliefert, und der Pfad, auf dem apply() eine vorhandene Zeile per + // preg_replace ersetzt statt sie nur anzuhängen. + file_put_contents($this->envPath, "APP_URL=https://app.example.test\nSITE_HOST=\n"); + + // `$1` und `\1` sind das, was preg_replace im ERSATZ als Rückverweis + // deutet und still verschluckt. Diese Datei hält jedes Geheimnis der + // Installation — was hier ankommt, muss ankommen wie eingegeben. + $this->artisan('clupilot:bind-hosts', [ + '--site' => 'www.example.test$1,foo\1bar', + '--force' => true, + ])->assertSuccessful(); + + expect(file_get_contents($this->envPath)) + ->toContain('SITE_HOST=www.example.test$1,foo\1bar'); +});