Files
fotospiel-app/app/Services/Security/PhotoSecurityScanner.php
Codex Agent fa33e7cbcf Fix Event & EventType resource issues and apply formatting
- Fix EventType deletion error handling (constraint violations)
- Fix Event update error (package_id column missing)
- Fix Event Type dropdown options (JSON display issue)
- Fix EventPackagesRelationManager query error
- Add missing translations for deletion errors
- Apply Pint formatting
2026-01-21 10:34:06 +01:00

195 lines
6.0 KiB
PHP

<?php
namespace App\Services\Security;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
class PhotoSecurityScanner
{
public function scan(string $disk, ?string $relativePath): array
{
if (! $relativePath) {
return [
'status' => 'error',
'message' => 'Missing path for antivirus scan.',
];
}
if (! config('security.antivirus.enabled', false)) {
return [
'status' => 'skipped',
'message' => 'Antivirus scanning disabled.',
];
}
[$absolutePath, $message] = $this->resolveAbsolutePath($disk, $relativePath);
if (! $absolutePath) {
return [
'status' => 'skipped',
'message' => $message ?? 'Unable to resolve path for antivirus.',
];
}
$binary = config('security.antivirus.binary', '/usr/bin/clamscan');
$arguments = config('security.antivirus.arguments', '--no-summary');
$timeout = config('security.antivirus.timeout', 60);
$command = $binary.' '.$arguments.' '.escapeshellarg($absolutePath);
try {
$process = Process::fromShellCommandline($command);
$process->setTimeout($timeout);
$process->run();
if ($process->isSuccessful()) {
return [
'status' => 'clean',
'message' => trim($process->getOutput()),
];
}
if ($process->getExitCode() === 1) {
return [
'status' => 'infected',
'message' => trim($process->getOutput() ?: $process->getErrorOutput()),
];
}
return [
'status' => 'error',
'message' => trim($process->getErrorOutput() ?: 'Unknown antivirus error.'),
];
} catch (ProcessFailedException $exception) {
return [
'status' => 'error',
'message' => $exception->getMessage(),
];
} catch (\Throwable $exception) {
Log::warning('[PhotoSecurity] Antivirus scan failed', [
'disk' => $disk,
'path' => $relativePath,
'error' => $exception->getMessage(),
]);
return [
'status' => 'error',
'message' => $exception->getMessage(),
];
}
}
public function stripExif(string $disk, ?string $relativePath): array
{
if (! config('security.exif.strip', true)) {
return [
'status' => 'skipped',
'message' => 'EXIF stripping disabled.',
];
}
if (! $relativePath) {
return [
'status' => 'error',
'message' => 'Missing path for EXIF stripping.',
];
}
[$absolutePath, $message] = $this->resolveAbsolutePath($disk, $relativePath);
if (! $absolutePath) {
return [
'status' => 'skipped',
'message' => $message ?? 'Unable to resolve path for EXIF stripping.',
];
}
$extension = strtolower(pathinfo($absolutePath, PATHINFO_EXTENSION));
if (! in_array($extension, ['jpg', 'jpeg', 'png', 'webp'], true)) {
return [
'status' => 'skipped',
'message' => 'Unsupported format for EXIF stripping.',
];
}
try {
$contents = @file_get_contents($absolutePath);
if ($contents === false) {
throw new \RuntimeException('Unable to read file for EXIF stripping.');
}
$image = @imagecreatefromstring($contents);
if (! $image) {
return [
'status' => 'skipped',
'message' => 'Unable to decode image for EXIF stripping.',
];
}
$result = match ($extension) {
'png' => imagepng($image, $absolutePath),
'webp' => function_exists('imagewebp') ? imagewebp($image, $absolutePath) : false,
default => imagejpeg($image, $absolutePath, 90),
};
imagedestroy($image);
if (! $result) {
return [
'status' => 'error',
'message' => 'Failed to re-encode image without EXIF.',
];
}
return [
'status' => 'stripped',
'message' => 'EXIF metadata removed via re-encode.',
];
} catch (\Throwable $exception) {
Log::warning('[PhotoSecurity] EXIF stripping failed', [
'disk' => $disk,
'path' => $relativePath,
'error' => $exception->getMessage(),
]);
return [
'status' => 'error',
'message' => $exception->getMessage(),
];
}
}
/**
* @return array{0: string|null, 1: string|null}
*/
private function resolveAbsolutePath(string $disk, string $relativePath): array
{
try {
$storage = Storage::disk($disk);
if (! method_exists($storage, 'path')) {
return [null, 'Storage driver does not expose local paths.'];
}
$absolute = $storage->path($relativePath);
if (! file_exists($absolute)) {
return [null, 'File not found on disk.'];
}
return [$absolute, null];
} catch (\Throwable $exception) {
Log::warning('[PhotoSecurity] Unable to resolve absolute path', [
'disk' => $disk,
'path' => $relativePath,
'error' => $exception->getMessage(),
]);
return [null, $exception->getMessage()];
}
}
}