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'); } }