More robust

This commit is contained in:
julien
2026-03-27 20:14:11 +01:00
parent 75ec966435
commit 68c547ddcb
25 changed files with 474 additions and 224 deletions

View File

@@ -4,6 +4,10 @@ declare(strict_types=1);
class Media extends DB\SQL\Mapper
{
private const MAX_WIDTH = 8000;
private const MAX_HEIGHT = 8000;
private const MAX_PIXELS = 40_000_000;
public function __construct(DB\SQL $db)
{
parent::__construct($db, 'media');
@@ -25,11 +29,40 @@ class Media extends DB\SQL\Mapper
public function all(): array
{
return array_map(
fn (self $m): array => $this->decorate($m->cast()),
fn(self $m): array => $this->decorate($m->cast()),
$this->find(null, ['order' => 'created_at DESC, id DESC']) ?: []
);
}
public function paginateLibrary(int $page = 1, int $perPage = 24): array
{
$result = $this->paginate(
max(0, $page - 1),
$perPage,
null,
['order' => 'created_at DESC, id DESC']
);
return [
'items' => array_map(fn(self $m): array => $this->decorate($m->cast()), $result['subset']),
'page' => max(1, min($page, $result['count'] ?: 1)),
'pages' => $result['count'] ?: 1,
];
}
public function latest(int $limit = 60): array
{
return array_map(
fn(self $m): array => $this->decorate($m->cast()),
$this->find(null, ['order' => 'created_at DESC, id DESC', 'limit' => $limit]) ?: []
);
}
public function countAll(): int
{
return $this->count();
}
public function findById(int $id): ?array
{
if ($id <= 0) {
@@ -50,36 +83,50 @@ class Media extends DB\SQL\Mapper
// pour dériver un texte alternatif lisible.
public function upload(string $srcPath, string $originalName = ''): int
{
// Image::dump() gère le chargement et la compression.
// Attention : en JPEG, la transparence n'est pas conservée.
// $path='' indique à F3 que le chemin est absolu.
$target = null;
$image = null;
try {
$img = new Image($srcPath, false, '');
$meta = self::inspectUpload($srcPath);
$image = self::openImageResource($srcPath, $meta['mime']);
[$format, $extension] = self::targetFormat($meta['mime']);
$fileName = bin2hex(random_bytes(16)) . '.' . $extension;
$target = app_public_media_dir() . '/' . $fileName;
self::writeImage($image, $target, $format);
$this->db->begin();
try {
$this->reset();
$this->file_name = $fileName;
$this->alt = $originalName !== '' ? self::altFromFilename($originalName) : '';
$this->width = $meta['width'];
$this->height = $meta['height'];
$this->created_at = app_now();
$this->save();
$this->db->commit();
} catch (Throwable $e) {
$this->db->rollback();
if ($target !== null && is_file($target)) {
@unlink($target);
}
throw $e;
}
return (int) $this->get('id');
} catch (RuntimeException $e) {
throw $e;
} catch (Throwable) {
throw new RuntimeException('Fichier image invalide ou format source non supporté.');
}
$data = $img->dump('jpeg', 85);
$fileName = bin2hex(random_bytes(16)) . '.jpg';
$target = app_public_media_dir() . '/' . $fileName;
if (!Base::instance()->write($target, $data)) {
throw new RuntimeException('Impossible d\'enregistrer cette image.');
} finally {
if ($image instanceof GdImage) {
imagedestroy($image);
}
if (is_file($srcPath)) {
@unlink($srcPath);
}
}
// Supprimer le fichier intermédiaire déposé par Web::receive().
@unlink($srcPath);
$this->reset();
$this->file_name = $fileName;
$this->alt = $originalName !== '' ? self::altFromFilename($originalName) : '';
$this->width = $img->width();
$this->height = $img->height();
$this->created_at = app_now();
$this->save();
return (int) $this->get('id');
}
public function updateAlt(int $id, string $alt): void
@@ -119,6 +166,83 @@ class Media extends DB\SQL\Mapper
}
}
private static function inspectUpload(string $srcPath): array
{
if (!is_file($srcPath)) {
throw new RuntimeException('Fichier image introuvable.');
}
$info = @getimagesize($srcPath);
if (!is_array($info)) {
throw new RuntimeException('Fichier image invalide ou format source non supporté.');
}
$width = (int) ($info[0] ?? 0);
$height = (int) ($info[1] ?? 0);
$mime = strtolower((string) ($info['mime'] ?? ''));
if (!in_array($mime, ['image/jpeg', 'image/png', 'image/webp'], true)) {
throw new RuntimeException('Format non supporté. Utilise JPG, PNG ou WebP.');
}
if ($width <= 0 || $height <= 0) {
throw new RuntimeException('Dimensions image invalides.');
}
if ($width > self::MAX_WIDTH || $height > self::MAX_HEIGHT || ($width * $height) > self::MAX_PIXELS) {
throw new RuntimeException('Image trop grande. Limite : 8000 × 8000 px et 40 mégapixels.');
}
return [
'width' => $width,
'height' => $height,
'mime' => $mime,
];
}
private static function openImageResource(string $srcPath, string $mime): GdImage
{
$image = match ($mime) {
'image/jpeg' => @imagecreatefromjpeg($srcPath),
'image/png' => @imagecreatefrompng($srcPath),
'image/webp' => function_exists('imagecreatefromwebp') ? @imagecreatefromwebp($srcPath) : false,
default => false,
};
if (!$image instanceof GdImage) {
throw new RuntimeException('Fichier image invalide ou format source non supporté.');
}
return $image;
}
private static function targetFormat(string $mime): array
{
return match ($mime) {
'image/jpeg' => ['jpeg', 'jpg'],
'image/png', 'image/webp' => ['png', 'png'],
default => throw new RuntimeException('Format non supporté. Utilise JPG, PNG ou WebP.'),
};
}
private static function writeImage(GdImage $image, string $target, string $format): void
{
if ($format === 'png') {
if (function_exists('imagepalettetotruecolor') && !imageistruecolor($image)) {
imagepalettetotruecolor($image);
}
imagealphablending($image, false);
imagesavealpha($image, true);
$written = @imagepng($image, $target, 6);
} else {
$written = @imagejpeg($image, $target, 85);
}
if ($written !== true || !is_file($target)) {
throw new RuntimeException('Impossible d\'enregistrer cette image.');
}
}
// Dérive un texte alternatif lisible depuis le nom de fichier d'origine.
private static function altFromFilename(string $filename): string
{
@@ -142,15 +266,15 @@ class Media extends DB\SQL\Mapper
$alt = (string) $row['alt'];
return [
'id' => (int) $row['id'],
'file_name' => (string) $row['file_name'],
'alt' => $alt,
'width' => (int) $row['width'],
'height' => (int) $row['height'],
'created_at' => (string) $row['created_at'],
'id' => (int) $row['id'],
'file_name' => (string) $row['file_name'],
'alt' => $alt,
'width' => (int) $row['width'],
'height' => (int) $row['height'],
'created_at' => (string) $row['created_at'],
'created_at_label' => app_format_datetime_fr((string) $row['created_at']),
'url' => app_media_url((string) $row['file_name']),
'markdown' => '![' . $alt . '](media:' . $row['file_name'] . ')',
'url' => app_media_url((string) $row['file_name']),
'markdown' => '![' . $alt . '](media:' . $row['file_name'] . ')',
];
}
}