fix: traiter le deuxième lot de tickets

Refs #1 #2 #3 #13 #17
This commit is contained in:
Alexandre NOEL 2026-09-07 23:55:59 +02:00
commit 220c0ece82
18 changed files with 95 additions and 22 deletions

View file

@ -491,9 +491,9 @@ trait AnimalRecordActions
$felv = array_key_exists('felv', $_POST) ? (isset($_POST['felv']) ? 1 : 0) : null;
// Validations minimales
if ($name === '') {
if ($name === '' || mb_strlen($name) > 150) {
http_response_code(400);
echo h(t('error.name_required'));
echo h($name === '' ? t('error.name_required') : t('animal.name_too_long'));
return;
}
if ($breed === '' && SpeciesService::isCat($species)) {

View file

@ -68,6 +68,10 @@ final class AuthController
$error = t('install.association_required');
} elseif ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
$error = t('settings.invalid_email');
} elseif (!Ids::validRna((string) ($_POST['association_rna'] ?? ''))) {
$error = t('settings.invalid_rna');
} elseif (!Ids::validSiret((string) ($_POST['association_siret'] ?? ''))) {
$error = t('settings.invalid_siret');
} elseif (!preg_match('/^[a-zA-Z0-9._-]{3,40}$/', $username)) {
$error = t('auth.invalid_username');
} elseif ($display === '') {

View file

@ -46,6 +46,14 @@ final class SettingsController
echo h(t('settings.invalid_email'));
return;
}
if (!Ids::validRna($values['association_rna'])) {
header('Location: /settings/general?error=' . rawurlencode(t('settings.invalid_rna')));
exit();
}
if (!Ids::validSiret($values['association_siret'])) {
header('Location: /settings/general?error=' . rawurlencode(t('settings.invalid_siret')));
exit();
}
$values['quarantine_days'] = (string) max(1, min(90, (int) ($_POST['quarantine_days'] ?? 15)));
$values['backup_retention'] = (string) max(1, min(50, (int) ($_POST['backup_retention'] ?? 10)));
$values['app_language'] = isset(I18n::LOCALES[(string) ($_POST['app_language'] ?? 'fr')])
@ -230,6 +238,7 @@ final class SettingsController
$color = (string) ($row['color'] ?? '#6c757d');
if (
$name === '' ||
mb_strlen($name) > 100 ||
!in_array($type, ['collective', 'boxes'], true) ||
!in_array($status, ['refuge', 'soin', 'quarantaine'], true) ||
!preg_match('/^#[0-9a-fA-F]{6}$/', $color)
@ -253,6 +262,9 @@ final class SettingsController
$new = (array) ($_POST['new_room'] ?? []);
$newName = trim((string) ($new['name'] ?? ''));
if ($newName !== '') {
if (mb_strlen($newName) > 100) {
throw new RuntimeException(t('rooms.name_too_long'));
}
$code = ShelterRoomService::normalizeCode((string) ($new['code'] ?? $newName));
if (strlen($code) < 2) {
throw new RuntimeException(t('rooms.invalid'));
@ -297,6 +309,9 @@ final class SettingsController
$known = $existing->fetchAll(PDO::FETCH_ASSOC);
$kept = [];
foreach ($names as $index => $name) {
if (mb_strlen($name) > 100) {
throw new RuntimeException(t('rooms.box_name_too_long'));
}
$position = ($index + 1) * 10;
if (isset($known[$index])) {
$id = (int) $known[$index]['id'];

View file

@ -4,6 +4,29 @@ declare(strict_types=1);
final class Ids
{
public static function validRna(string $value): bool
{
$value = strtoupper(preg_replace('/\s+/', '', trim($value)) ?? '');
return $value === '' || preg_match('/^W[0-9]{9}$/', $value) === 1;
}
public static function validSiret(string $value): bool
{
$digits = preg_replace('/\D+/', '', trim($value)) ?? '';
if ($digits === '') {
return true;
}
if (strlen($digits) !== 14) {
return false;
}
$sum = 0;
foreach (str_split($digits) as $index => $digit) {
$number = (int) $digit * ($index % 2 === 0 ? 2 : 1);
$sum += $number > 9 ? $number - 9 : $number;
}
return $sum % 10 === 0;
}
public static function slugifyUpper(string $s): string
{
$s = trim($s);

View file

@ -6,6 +6,14 @@ final class ImageService
{
private const ALLOWED = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', 'image/gif' => 'gif'];
public static function processor(): ?string
{
if (self::magickBinary() !== null) {
return 'ImageMagick';
}
return extension_loaded('gd') ? 'GD' : null;
}
public static function inspect(string $path, int $maxBytes = 20_000_000): array
{
if (!is_file($path) || filesize($path) > $maxBytes) {

View file

@ -57,17 +57,14 @@ final class InstallationService
? 'Connecteurs externes disponibles'
: 'Extension cURL absente : I-CAD et géocodage indisponibles',
);
$imageProcessor = ImageService::processor();
self::add(
$checks,
'image',
extension_loaded('imagick') || extension_loaded('gd'),
$imageProcessor !== null,
'recommended',
'Traitement des images',
extension_loaded('imagick')
? 'ImageMagick disponible'
: (extension_loaded('gd')
? 'GD disponible'
: 'ImageMagick ou GD conseillé'),
$imageProcessor !== null ? $imageProcessor . ' disponible' : 'ImageMagick ou GD conseillé',
);
if ($withFilesystem) {
foreach (self::directories() as $key => $path) {

View file

@ -74,7 +74,7 @@ final class SystemHealthService
);
$backups = BackupService::list();
if (!$backups) {
$checks[] = self::check('error', 'continuity', t('health.last_backup'), t('health.no_backup'));
$checks[] = self::check('warning', 'continuity', t('health.last_backup'), t('health.no_backup'));
} else {
$latest = $backups[0];
$date = DateTimeImmutable::createFromFormat(

View file

@ -95,7 +95,7 @@ $estMonths = $birth_estimated_months > 0 ? ($birth_estimated_months % 12) : '';
<div class="col-12 col-md-8">
<label class="form-label"><?= h(t('common.name')) ?></label>
<input class="form-control" name="name" value="<?= h($animal['name'] ?? '') ?>"
required>
maxlength="150" required>
</div>
<div class="col-12 col-md-4">

View file

@ -109,9 +109,11 @@
value="<?= h($_POST['association_name'] ?? '') ?>" required></div>
<div class="col-md-6"><label class="form-label"><?= h(t('settings.rna')) ?></label><input
class="form-control" name="association_rna"
pattern="W[0-9]{9}" maxlength="10"
value="<?= h($_POST['association_rna'] ?? '') ?>"></div>
<div class="col-md-6"><label class="form-label"><?= h(t('settings.siret')) ?></label><input
class="form-control" name="association_siret"
inputmode="numeric" maxlength="17"
value="<?= h($_POST['association_siret'] ?? '') ?>"></div>
<div class="col-12"><label class="form-label"><?= h(t('settings.address')) ?></label><input
class="form-control" name="association_address"

View file

@ -31,10 +31,10 @@
<div class="col-lg-6">
<section class="card h-100">
<div class="card-body">
<span class="badge bg-blue-lt mb-2">1.0.0-rc.7</span>
<h3>Premier lot de tickets</h3>
<p class="text-muted">Géocodage fiabilisé, doublons expliqués, tournées plus lisibles et animaux
adoptables publiés dans la démonstration.</p>
<span class="badge bg-blue-lt mb-2">1.0.0-rc.8</span>
<h3>Deuxième lot de tickets</h3>
<p class="text-muted">Diagnostic système clarifié, identifiants administratifs contrôlés et
longueurs des principaux noms encadrées.</p>
</div>
</section>
</div>

View file

@ -19,9 +19,10 @@
required></div>
<div class="col-md-3"><label class="form-label"><?= h(t('settings.rna')) ?></label><input
class="form-control" name="association_rna" value="<?= h($settings['association_rna']) ?>"
placeholder="W…"></div>
pattern="W[0-9]{9}" maxlength="10" placeholder="W123456789"></div>
<div class="col-md-3"><label class="form-label"><?= h(t('settings.siret')) ?></label><input
class="form-control" name="association_siret" value="<?= h($settings['association_siret']) ?>">
class="form-control" name="association_siret" inputmode="numeric" maxlength="17"
value="<?= h($settings['association_siret']) ?>">
</div>
<div class="col-12"><label class="form-label"><?= h(t('settings.address')) ?></label><input
class="form-control" name="association_address"

View file

@ -19,7 +19,7 @@
<div class="card-body row g-3">
<div class="col-md-8"><label class="form-label"><?= h(t('common.name')) ?></label><input
class="form-control" name="rooms[<?= (int)$room['id'] ?>][name]"
value="<?= h($room['name']) ?>" required></div>
value="<?= h($room['name']) ?>" maxlength="100" required></div>
<div class="col-md-4"><label class="form-label"><?= h(t('rooms.color')) ?></label><input
class="form-control form-control-color w-100" type="color"
name="rooms[<?= (int)$room['id'] ?>][color]" value="<?= h($room['color']) ?>"></div>
@ -40,7 +40,7 @@
value="<?= (int)$room['sort_order'] ?>"></div>
<div class="col-12"><label class="form-label"><?= h(t('rooms.boxes')) ?></label><textarea
class="form-control" rows="5" name="rooms[<?= (int)$room['id'] ?>][boxes]"
placeholder="<?= h(t('rooms.one_box_per_line')) ?>"><?= h($boxNames) ?></textarea>
placeholder="<?= h(t('rooms.one_box_per_line')) ?>" data-line-maxlength="100"><?= h($boxNames) ?></textarea>
<div class="form-hint"><?= h(t('rooms.boxes_help')) ?></div>
</div>
<div class="col-12"><label class="form-check"><input class="form-check-input" type="checkbox"
@ -57,7 +57,7 @@
</div>
<div class="card-body row g-3">
<div class="col-md-4"><label class="form-label"><?= h(t('common.name')) ?></label><input
class="form-control" name="new_room[name]"></div>
class="form-control" name="new_room[name]" maxlength="100"></div>
<div class="col-md-3"><label class="form-label"><?= h(t('rooms.type')) ?></label><select
class="form-select" name="new_room[room_type]">
<option value="collective"><?= h(t('rooms.collective')) ?></option>
@ -74,7 +74,7 @@
value="#6c757d"></div>
<div class="col-12"><label class="form-label"><?= h(t('rooms.boxes')) ?></label><textarea
class="form-control" name="new_room[boxes]" rows="3"
placeholder="<?= h(t('rooms.one_box_per_line')) ?>"></textarea></div>
placeholder="<?= h(t('rooms.one_box_per_line')) ?>" data-line-maxlength="100"></textarea></div>
<div class="col-12"><label class="form-check"><input class="form-check-input" type="checkbox"
name="new_room[bulk_validation]"><span
class="form-check-label"><?= h(t('rooms.bulk_validation')) ?></span></label></div>