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
parent
7a6d20cbed
commit
359aa86aea
10
CHANGELOG.md
10
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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue