<?php
 namespace App\Services; class AntivirusService { public const CLEAN = 'clean'; public const INFECTED = 'infected'; public const FAILED = 'scan_failed'; public function scan(string $absolutePath): array { if (! setting('antivirus_enabled', false)) { return ['status' => self::CLEAN, 'message' => 'antivirus disabled']; } if (! is_readable($absolutePath)) { return ['status' => self::FAILED, 'message' => 'file not readable']; } $host = env('CLAMAV_HOST', 'clamav'); $port = (int) env('CLAMAV_PORT', 3310); $socket = @fsockopen($host, $port, $errno, $errstr, 5); if (! $socket) { return ['status' => self::FAILED, 'message' => "clamd unreachable: {$errstr}"]; } try { stream_set_timeout($socket, 30); fwrite($socket, "zINSTREAM\0"); $fh = fopen($absolutePath, 'rb'); if (! $fh) { return ['status' => self::FAILED, 'message' => 'cannot open file']; } while (! feof($fh)) { $chunk = fread($fh, 8192); if ($chunk === '' || $chunk === false) { break; } fwrite($socket, pack('N', strlen($chunk)).$chunk); } fclose($fh); fwrite($socket, pack('N', 0)); $response = trim((string) fgets($socket)); } finally { fclose($socket); } if ($response === '' || str_contains($response, 'ERROR')) { return ['status' => self::FAILED, 'message' => $response ?: 'empty clamd response']; } if (str_contains($response, 'FOUND')) { return ['status' => self::INFECTED, 'message' => $response]; } if (str_contains($response, 'OK')) { return ['status' => self::CLEAN, 'message' => null]; } return ['status' => self::FAILED, 'message' => $response]; } } 