Globinours/app/Services/PrivateMediaService.php

148 lines
5.4 KiB
PHP

<?php
declare(strict_types=1);
final class PrivateMediaService
{
public static function root(): string
{
return dirname(__DIR__, 2) . '/data/private-media';
}
public static function animalDir(int $animalId): string
{
return self::root() . '/animals/' . $animalId;
}
public static function medicalDir(int $animalId): string
{
return self::animalDir($animalId) . '/meds';
}
public static function publicDir(int $animalId): string
{
return dirname(__DIR__, 2) . '/public/media/animals/' . $animalId;
}
public static function ensure(string $dir): void
{
if (!is_dir($dir) && !mkdir($dir, 0700, true) && !is_dir($dir)) {
throw new RuntimeException(t('service.image.directory_failed'));
}
@chmod($dir, 0700);
}
public static function moveVisibility(int $animalId, string $filename, bool $toPublic): void
{
$filename = basename($filename);
if ($filename === '' || $filename === '.' || $filename === '..') {
throw new RuntimeException(t('service.private_media.invalid_name'));
}
$from = $toPublic ? self::animalDir($animalId) : self::publicDir($animalId);
$to = $toPublic ? self::publicDir($animalId) : self::animalDir($animalId);
$source = $from . '/' . $filename;
$target = $to . '/' . $filename;
if (!is_file($source)) {
if (is_file($target)) {
return;
}
throw new RuntimeException(t('service.private_media.not_found'));
}
self::ensure($to);
if (!rename($source, $target)) {
throw new RuntimeException(t('service.private_media.move_failed'));
}
@chmod($target, $toPublic ? 0644 : 0600);
}
public static function migrateLegacy(): void
{
$db = DB::pdo();
$private = $db
->query('SELECT animal_id,filename FROM animal_photos WHERE is_public=0')
->fetchAll(PDO::FETCH_ASSOC);
foreach ($private as $row) {
self::moveIfPresent(
self::publicDir((int) $row['animal_id']) . '/' . basename((string) $row['filename']),
self::animalDir((int) $row['animal_id']) . '/' . basename((string) $row['filename']),
);
}
$medical = $db->query('SELECT mp.animal_id,mp.filename FROM medical_photos mp')->fetchAll(PDO::FETCH_ASSOC);
foreach ($medical as $row) {
self::moveIfPresent(
self::publicDir((int) $row['animal_id']) . '/meds/' . basename((string) $row['filename']),
self::medicalDir((int) $row['animal_id']) . '/' . basename((string) $row['filename']),
);
}
}
private static function moveIfPresent(string $source, string $target): void
{
if (!is_file($source) || is_file($target)) {
return;
}
self::ensure(dirname($target));
if (rename($source, $target)) {
@chmod($target, 0600);
}
}
public static function send(): void
{
$kind = (string) ($_GET['kind'] ?? 'photo');
$id = (int) ($_GET['id'] ?? 0);
if ($id <= 0) {
http_response_code(404);
return;
}
$db = DB::pdo();
if ($kind === 'medical') {
if (!PermissionService::can('medical', 'view')) {
http_response_code(403);
return;
}
$s = $db->prepare(
'SELECT mp.id,mp.animal_id,mp.filename FROM medical_photos mp JOIN animals a ON a.id=mp.animal_id WHERE mp.id=? AND a.deleted_at IS NULL',
);
$s->execute([$id]);
$row = $s->fetch(PDO::FETCH_ASSOC);
$path = $row ? self::medicalDir((int) $row['animal_id']) . '/' . basename((string) $row['filename']) : '';
} else {
if (!PermissionService::can('animals', 'view')) {
http_response_code(403);
return;
}
$s = $db->prepare(
'SELECT ap.id,ap.animal_id,ap.filename,ap.care_round_id FROM animal_photos ap JOIN animals a ON a.id=ap.animal_id WHERE ap.id=? AND ap.is_public=0 AND a.deleted_at IS NULL',
);
$s->execute([$id]);
$row = $s->fetch(PDO::FETCH_ASSOC);
if ($row && !empty($row['care_round_id']) && !PermissionService::can('medical', 'view')) {
http_response_code(403);
return;
}
$path = $row ? self::animalDir((int) $row['animal_id']) . '/' . basename((string) $row['filename']) : '';
}
if (empty($row) || !is_file($path)) {
http_response_code(404);
return;
}
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($path) ?: 'application/octet-stream';
PrivacyService::logAccess(
'private_' . $kind,
$id,
(int) $row['animal_id'],
basename((string) $row['filename']),
);
AuditService::log(
'private_file_viewed',
'/private-media',
'Média privé consulté',
'animal',
(int) $row['animal_id'],
['kind' => $kind, 'media_id' => $id],
);
header('Content-Type: ' . $mime);
header('Content-Length: ' . filesize($path));
header('Cache-Control: private,no-store');
header('X-Content-Type-Options: nosniff');
readfile($path);
}
}