feat: add image content validator

This commit is contained in:
Keith Solomon
2026-07-06 18:32:50 -05:00
parent 288c42e176
commit 6399e2d86c
4 changed files with 286 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace BattleForge\Application;
final class ImageValidator
{
/**
* @return array{string, string} [canonicalMime, extension] for a recognized image, or null.
*/
private static function detect(string $header): ?array
{
if (substr($header, 0, 8) === "\x89PNG\r\n\x1a\n") {
return ['image/png', 'png'];
}
if (substr($header, 0, 3) === "\xff\xd8\xff") {
return ['image/jpeg', 'jpg'];
}
if (substr($header, 0, 6) === 'GIF87a' || substr($header, 0, 6) === 'GIF89a') {
return ['image/gif', 'gif'];
}
if (substr($header, 0, 4) === 'RIFF' && substr($header, 8, 4) === 'WEBP') {
return ['image/webp', 'webp'];
}
return null;
}
public static function validate(
string $tempPath,
string $declaredMime,
int $maxBytes = 2_000_000,
int $maxDimension = 512,
): ValidatedImage {
if (!is_file($tempPath)) {
throw new InvalidImageException('Upload is not a file.');
}
$size = filesize($tempPath);
if ($size === false || $size > $maxBytes) {
throw new InvalidImageException("File exceeds the {$maxBytes} byte limit.");
}
$handle = fopen($tempPath, 'rb');
if ($handle === false) {
throw new InvalidImageException('Could not read upload.');
}
$header = fread($handle, 12);
fclose($handle);
if ($header === false || strlen($header) < 12) {
throw new InvalidImageException('Upload is too small to be an image.');
}
$detected = self::detect($header);
if ($detected === null) {
throw new InvalidImageException('File is not a supported image type.');
}
[$canonical, $extension] = $detected;
if ($declaredMime !== $canonical) {
throw new InvalidImageException('Declared MIME does not match file contents.');
}
$info = getimagesize($tempPath);
if ($info === false) {
throw new InvalidImageException('Image is corrupt or unreadable.');
}
[$width, $height] = $info;
if ($width > $maxDimension || $height > $maxDimension) {
throw new InvalidImageException("Image dimensions exceed the {$maxDimension} pixel limit.");
}
return new ValidatedImage($canonical, $extension);
}
}
+11
View File
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace BattleForge\Application;
use RuntimeException;
final class InvalidImageException extends RuntimeException
{
}
+14
View File
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace BattleForge\Application;
final readonly class ValidatedImage
{
public function __construct(
public string $canonicalMime,
public string $extension,
) {
}
}