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 <noreply@anthropic.com>
feat/v1-foundation v0.9.54
boban 2026-06-22 05:49:05 +02:00
parent 7a6d20cbed
commit 359aa86aea
5 changed files with 75 additions and 4 deletions

View File

@ -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

View File

@ -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();

View File

@ -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();
}

View File

@ -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

View File

@ -0,0 +1,58 @@
<?php
namespace Tests\Feature;
use App\Livewire\Modals\FileEditor;
use App\Models\Server;
use App\Models\User;
use App\Services\FleetService;
use App\Support\Ssh\Sftp;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Mockery;
use Tests\TestCase;
/**
* Binary / non-UTF-8 file content must never reach the editor's public $content property: bound into
* Livewire's JSON snapshot, invalid UTF-8 makes json_encode fail and the client sees an empty response
* ("undefined is not valid JSON"). The editor blanks it and shows a binary notice instead.
*/
class FileEditorBinaryTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->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");
}
}