From 359aa86aeadc29108a40da5e6778748e6bc6f121 Mon Sep 17 00:00:00 2001 From: boban Date: Mon, 22 Jun 2026 05:49:05 +0200 Subject: [PATCH] fix(files): binary file content no longer breaks the Livewire snapshot (v0.9.54) Opening a binary / non-UTF-8 file in the file manager threw "undefined is not valid JSON". The editor bound the raw file bytes to its public $content property; Livewire JSON-encodes the component snapshot, and invalid UTF-8 makes PHP's json_encode return false, so the client received an empty response and JSON.parse("undefined") failed (the trace in mergeNewSnapshot/deepClone). Fixed at both layers: FleetService::readFile blanks the content when it detects binary/non-UTF-8 (returns the binary flag instead), and FileEditor::load only binds content for non-binary, non-oversize files. Binary/oversize files show their existing notice; the snapshot is always valid UTF-8. Tests: load blanks binary content (keeps the snapshot valid) and keeps valid text content. 374 pass, Pint clean. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 10 ++++- app/Livewire/Modals/FileEditor.php | 5 ++- app/Services/FleetService.php | 4 +- config/clusev.php | 2 +- tests/Feature/FileEditorBinaryTest.php | 58 ++++++++++++++++++++++++++ 5 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 tests/Feature/FileEditorBinaryTest.php 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"); + } +}