clusev/app/Livewire/Modals/FileEditor.php

180 lines
5.0 KiB
PHP

<?php
namespace App\Livewire\Modals;
use App\Models\AuditEvent;
use App\Models\Server;
use App\Services\FleetService;
use App\Support\Ssh\Sftp;
use Illuminate\Support\Facades\Auth;
use LivewireUI\Modal\ModalComponent;
use Throwable;
/**
* View / edit a remote text file over SFTP. Loads lazily (wire:init), refuses
* binary or oversized files, and writes back on save with an AuditEvent.
* Image files are shown as a read-only base64 preview instead of the editor.
*/
class FileEditor extends ModalComponent
{
/** Image preview is capped — larger images are refused (base64 inflates ~33%). */
private const IMAGE_MAX_BYTES = 5_242_880; // 5 MB
/** Extensions rendered as an inline image preview (read-only). */
private const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'ico', 'avif'];
public int $serverId;
public string $path;
public string $name;
public string $content = '';
public bool $binary = false;
public bool $tooBig = false;
public bool $loaded = false;
public ?string $error = null;
/** base64 `data:` URI for image previews; null for text files. */
public ?string $dataUri = null;
public function mount(int $serverId, string $path, string $name): void
{
$this->serverId = $serverId;
$this->path = $path;
$this->name = $name;
}
public static function modalMaxWidth(): string
{
return 'lg';
}
/** True when the opened file is an image (preview, not editable). */
public function isImage(): bool
{
return in_array($this->extension(), self::IMAGE_EXTENSIONS, true);
}
private function extension(): string
{
return strtolower(pathinfo($this->name, PATHINFO_EXTENSION));
}
/** MIME type for the image preview data URI, keyed off the extension. */
private function imageMime(): string
{
return match ($this->extension()) {
'svg' => 'image/svg+xml',
'jpg', 'jpeg' => 'image/jpeg',
'ico' => 'image/x-icon',
default => 'image/'.$this->extension(),
};
}
public function load(FleetService $fleet, Sftp $sftp): void
{
$server = Server::find($this->serverId);
if (! $server) {
$this->error = __('common.server_not_found');
$this->loaded = true;
return;
}
try {
if ($this->isImage()) {
$this->loadImage($server, $sftp);
} else {
$r = $fleet->readFile($server, $this->path);
$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();
}
$this->loaded = true;
}
/**
* Read the image bytes over SFTP and expose them as a base64 data URI.
* Refuses files larger than IMAGE_MAX_BYTES (checked before download).
*/
private function loadImage(Server $server, Sftp $sftp): void
{
$sftp->connect($server);
try {
if ($sftp->size($this->path) > self::IMAGE_MAX_BYTES) {
$this->tooBig = true;
return;
}
$bytes = $sftp->get($this->path);
if ($bytes === '') {
$this->error = __('modals.file_editor.image_read_failed');
return;
}
$this->dataUri = 'data:'.$this->imageMime().';base64,'.base64_encode($bytes);
} finally {
$sftp->disconnect();
}
}
public function save(FleetService $fleet): void
{
$this->error = null;
// Images are a read-only preview — never write them back.
if ($this->isImage()) {
return;
}
$server = Server::find($this->serverId);
if (! $server) {
$this->error = __('common.server_not_found');
return;
}
try {
$fleet->writeFile($server, $this->path, $this->content);
} catch (Throwable $e) {
$this->error = $e->getMessage();
return;
}
AuditEvent::create([
'user_id' => Auth::id(),
'server_id' => $server->id,
'actor' => Auth::user()?->name ?? 'system',
'action' => 'file.edit',
'target' => $this->path,
'ip' => request()->ip(),
]);
$this->dispatch('fileSaved');
$this->dispatch('notify', message: __('modals.file_editor.notify_saved', ['name' => $this->name]));
$this->closeModal();
}
public function render()
{
return view('livewire.modals.file-editor');
}
}