diff --git a/CHANGELOG.md b/CHANGELOG.md index f0fba51..f0a4e22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,15 @@ getaggte Releases (Kanal `stable`, optional `beta`) — niemals Entwicklungs-Bui _Keine offenen Änderungen — der nächste Stand wird hier gesammelt und als `vX.Y.Z` getaggt._ -## [0.9.53] - 2026-06-22 +## [0.9.54] - 2026-06-22 + +### Behoben +- **Datei-Manager: „undefined is not valid JSON" beim Öffnen mancher Dateien.** Öffnete man eine + **Binär-/Nicht-UTF-8-Datei**, landeten deren Rohbytes in einer Livewire-Eigenschaft. Livewire + JSON-kodiert seinen Zustand, und ungültiges UTF-8 lässt `json_encode` scheitern → der Client bekam + eine leere Antwort und warf den Fehler. Binär-/zu-große Dateien werden jetzt nicht mehr als Inhalt + gebunden (es erscheint der Binär-Hinweis), sodass der Zustand immer gültiges JSON bleibt — sowohl + im Editor als auch in der zugrunde liegenden Datei-Lese-Funktion abgesichert. ### Behoben - **JavaScript-Fehler im Datei-Manager (und anderen Listen): „pending is not defined".** In Listen diff --git a/app/Livewire/Modals/FileEditor.php b/app/Livewire/Modals/FileEditor.php index c64b47d..386f9db 100644 --- a/app/Livewire/Modals/FileEditor.php +++ b/app/Livewire/Modals/FileEditor.php @@ -91,9 +91,12 @@ class FileEditor extends ModalComponent $this->loadImage($server, $sftp); } else { $r = $fleet->readFile($server, $this->path); - $this->content = $r['content']; $this->binary = $r['binary']; $this->tooBig = $r['tooBig']; + // Never bind raw non-UTF-8 bytes to a public property: Livewire JSON-encodes the + // snapshot and invalid UTF-8 makes json_encode fail, so the client receives an empty + // response ("undefined is not valid JSON"). Binary/oversize files show a notice instead. + $this->content = ($r['binary'] || $r['tooBig']) ? '' : $r['content']; } } catch (Throwable $e) { $this->error = $e->getMessage(); diff --git a/app/Services/FleetService.php b/app/Services/FleetService.php index a30b063..3134114 100644 --- a/app/Services/FleetService.php +++ b/app/Services/FleetService.php @@ -561,7 +561,9 @@ class FleetService $content = $size > $maxBytes ? '' : (string) $sftp->get($path); $binary = $content !== '' && (str_contains(substr($content, 0, 8000), "\0") || ! mb_check_encoding($content, 'UTF-8')); - return ['content' => $content, 'binary' => $binary, 'tooBig' => $size > $maxBytes]; + // Never hand back raw non-UTF-8 bytes: bound to a Livewire public property they break the + // JSON snapshot ("undefined is not valid JSON"). The caller renders a binary notice instead. + return ['content' => $binary ? '' : $content, 'binary' => $binary, 'tooBig' => $size > $maxBytes]; } finally { $sftp->disconnect(); } diff --git a/config/clusev.php b/config/clusev.php index b5066e2..9b619fe 100644 --- a/config/clusev.php +++ b/config/clusev.php @@ -2,7 +2,7 @@ return [ // First tagged release is v0.1.0 (semantic, not -dev). - 'version' => '0.9.53', + 'version' => '0.9.54', // Deployed commit + branch. install.sh bakes these into .env from the host's .git (the prod // image ships no .git); the Versions page prefers them and falls back to a live .git read in diff --git a/tests/Feature/FileEditorBinaryTest.php b/tests/Feature/FileEditorBinaryTest.php new file mode 100644 index 0000000..3dd485d --- /dev/null +++ b/tests/Feature/FileEditorBinaryTest.php @@ -0,0 +1,58 @@ +actingAs(User::factory()->create(['must_change_password' => false])); + $this->app->instance(Sftp::class, Mockery::mock(Sftp::class)); // unused for text files, just resolvable + } + + public function test_load_blanks_binary_content_to_keep_the_snapshot_valid(): void + { + $server = Server::create(['name' => 's', 'ip' => '10.0.0.9', 'ssh_port' => 22, 'status' => 'online']); + + $fleet = Mockery::mock(FleetService::class); + $fleet->shouldReceive('readFile')->andReturn(['content' => "\xff\xfe\x00\x80raw-binary", 'binary' => true, 'tooBig' => false]); + $this->app->instance(FleetService::class, $fleet); + + Livewire::test(FileEditor::class, ['serverId' => $server->id, 'path' => '/tmp/x.bin', 'name' => 'x.bin']) + ->call('load') + ->assertSet('binary', true) + ->assertSet('content', ''); // raw non-UTF-8 bytes never bound to the property + } + + public function test_load_keeps_valid_text_content(): void + { + $server = Server::create(['name' => 's', 'ip' => '10.0.0.9', 'ssh_port' => 22, 'status' => 'online']); + + $fleet = Mockery::mock(FleetService::class); + $fleet->shouldReceive('readFile')->andReturn(['content' => "hello = welt\n", 'binary' => false, 'tooBig' => false]); + $this->app->instance(FleetService::class, $fleet); + + Livewire::test(FileEditor::class, ['serverId' => $server->id, 'path' => '/etc/x.conf', 'name' => 'x.conf']) + ->call('load') + ->assertSet('binary', false) + ->assertSet('content', "hello = welt\n"); + } +}