<?php declare(strict_types=1); if (!defined('DASHBOARD_ROOT')) { http_response_code(403); exit('Direct access denied.'); } class ImageProcess { const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp']; const MAX_WIDTH = 1200; const MAX_HEIGHT = 1200; const THUMB_WIDTH = 400; const THUMB_HEIGHT = 400; const QUALITY = 85; public static function validate(array $file): array { $errors = []; if ($file['error'] !== UPLOAD_ERR_OK) { $errors[] = 'Upload failed with error code: ' . $file['error']; return $errors; } if ($file['size'] > MAX_UPLOAD_SIZE) { $errors[] = 'File exceeds maximum size of ' . (MAX_UPLOAD_SIZE / 1024 / 1024) . 'MB.'; } $finfo = new finfo(FILEINFO_MIME_TYPE); $mime = $finfo->file($file['tmp_name']); if (!in_array($mime, self::ALLOWED_TYPES, true)) { $errors[] = 'Invalid file type. Allowed: JPEG, PNG, WebP.'; } $imageInfo = @getimagesize($file['tmp_name']); if ($imageInfo === false) { $errors[] = 'File is not a valid image.'; } return $errors; } public static function quarantine(array $file): string { $ext = pathinfo($file['name'], PATHINFO_EXTENSION); $safeName = bin2hex(random_bytes(16)) . '.' . strtolower($ext); $quarantinePath = UPLOAD_QUARANTINE_DIR . '/' . $safeName; if (!move_uploaded_file($file['tmp_name'], $quarantinePath)) { throw new RuntimeException('Failed to move file to quarantine.'); } return $quarantinePath; } public static function stripMetadata(string $filePath): void { $image = self::loadImage($filePath); if ($image === null) return; self::saveImage($image, $filePath); imagedestroy($image); } public static function resize(string $filePath, int $maxW = self::MAX_WIDTH, int $maxH = self::MAX_HEIGHT): void { [$origW, $origH] = getimagesize($filePath); if ($origW <= $maxW && $origH <= $maxH) { return; } $ratio = min($maxW / $origW, $maxH / $origH); $newW = (int) round($origW * $ratio); $newH = (int) round($origH * $ratio); $source = self::loadImage($filePath); $dest = imagecreatetruecolor($newW, $newH); imagealphablending($dest, false); imagesavealpha($dest, true); imagecopyresampled($dest, $source, 0, 0, 0, 0, $newW, $newH, $origW, $origH); self::saveImage($dest, $filePath); imagedestroy($source); imagedestroy($dest); } public static function process(array $file, string $destDir = ''): string { $destDir = $destDir ?: UPLOAD_PRODUCTS_DIR; $errors = self::validate($file); if (!empty($errors)) { throw new RuntimeException(implode(' ', $errors)); } $quarantined = self::quarantine($file); self::stripMetadata($quarantined); self::resize($quarantined); $ext = strtolower(pathinfo($quarantined, PATHINFO_EXTENSION)); $finalName = bin2hex(random_bytes(16)) . '.' . $ext; $finalPath = $destDir . '/' . $finalName; if (!rename($quarantined, $finalPath)) { throw new RuntimeException('Failed to move processed image to destination.'); } return $finalName; } private static function loadImage(string $path): ?GdImage { $mime = mime_content_type($path); return match ($mime) { 'image/jpeg' => imagecreatefromjpeg($path), 'image/png' => imagecreatefrompng($path), 'image/webp' => imagecreatefromwebp($path), default => null, }; } private static function saveImage(GdImage $image, string $path): void { $mime = mime_content_type($path); match ($mime) { 'image/jpeg' => imagejpeg($image, $path, self::QUALITY), 'image/png' => imagepng($image, $path, 6), 'image/webp' => imagewebp($image, $path, self::QUALITY), default => imagejpeg($image, $path, self::QUALITY), }; } } 