Préparer la version 1.0.0-rc.4
This commit is contained in:
parent
0a482afe9d
commit
6e3beea211
38 changed files with 388 additions and 56 deletions
|
|
@ -22,6 +22,8 @@ final class AppSettings
|
|||
'app_language' => 'fr',
|
||||
'public_site_enabled' => '0',
|
||||
'public_site_theme' => 'warm',
|
||||
'public_site_name' => '',
|
||||
'public_site_custom_css' => '',
|
||||
'public_site_routing' => 'integrated',
|
||||
'public_site_home_title' => 'Ils attendent une famille',
|
||||
'public_site_home_text' => 'Découvrez les animaux actuellement proposés à l’adoption par notre association.',
|
||||
|
|
@ -35,6 +37,10 @@ final class AppSettings
|
|||
'public_site_contact_text' => 'Une question sur une adoption ou sur l’association ? Contactez-nous.',
|
||||
'public_site_facebook_url' => '',
|
||||
'public_site_instagram_url' => '',
|
||||
'public_site_mastodon_url' => '',
|
||||
'public_site_youtube_url' => '',
|
||||
'public_site_tiktok_url' => '',
|
||||
'public_site_bluesky_url' => '',
|
||||
'notification_email_enabled' => '0',
|
||||
'notification_email_frequency' => 'weekly',
|
||||
'notification_email_recipient' => '',
|
||||
|
|
|
|||
|
|
@ -87,11 +87,12 @@ final class IcadService
|
|||
throw new RuntimeException(t('service.icad.credentials_missing'));
|
||||
}
|
||||
|
||||
$cookie = sys_get_temp_dir() . '/twix_icad_' . bin2hex(random_bytes(6)) . '.cookie';
|
||||
$cookie = sys_get_temp_dir() . '/globinours_icad_' . bin2hex(random_bytes(6)) . '.cookie';
|
||||
|
||||
try {
|
||||
// 1) GET HOME => token _token du formulaire login ACTEUR
|
||||
$r1 = $this->req('https://www.i-cad.fr/', $cookie);
|
||||
$this->assertSuccess($r1, t('service.icad.home_unavailable'));
|
||||
|
||||
$token = $this->extractToken($r1['body'], '_token'); // prend le 1er _token trouvé
|
||||
|
||||
|
|
@ -109,16 +110,19 @@ final class IcadService
|
|||
'Origin: https://www.i-cad.fr',
|
||||
'Referer: https://www.i-cad.fr/',
|
||||
]);
|
||||
$this->assertSuccess($r2, t('service.icad.login_failed'));
|
||||
|
||||
// 3) Vérif session
|
||||
$r2b = $this->req('https://www.i-cad.fr/pro', $cookie);
|
||||
if (stripos($r2b['body'], 'Se connecter') !== false) {
|
||||
$this->assertSuccess($r2b, t('service.icad.login_failed'));
|
||||
if (stripos($r2b['body'], 'Se connecter') !== false || str_contains((string)$r2b['effective_url'], '/login')) {
|
||||
throw new RuntimeException(t('service.icad.login_failed'));
|
||||
}
|
||||
|
||||
// 4) GET recherche. I-CAD a renommé le formulaire
|
||||
// search_animal en animal_search en 2026 : on accepte les deux.
|
||||
$r3 = $this->req('https://www.i-cad.fr/identification/recherche', $cookie);
|
||||
$this->assertSuccess($r3, t('service.icad.search_unavailable'));
|
||||
[$searchForm, $searchToken] = $this->extractAnimalSearchForm($r3['body']);
|
||||
|
||||
// 5) POST recherche
|
||||
|
|
@ -136,6 +140,7 @@ final class IcadService
|
|||
'Origin: https://www.i-cad.fr',
|
||||
'Referer: https://www.i-cad.fr/identification/recherche',
|
||||
]);
|
||||
$this->assertSuccess($r4, t('service.icad.search_unavailable'));
|
||||
|
||||
$animalPath = $this->extractAnimalPath($r4);
|
||||
if (!$animalPath) {
|
||||
|
|
@ -144,6 +149,7 @@ final class IcadService
|
|||
|
||||
// 6) GET fiche animal
|
||||
$r5 = $this->req('https://www.i-cad.fr' . $animalPath, $cookie);
|
||||
$this->assertSuccess($r5, t('service.icad.record_unavailable'));
|
||||
$data = $this->parseAnimalHtml($r5['body'], $animalPath);
|
||||
|
||||
$this->saveCache($animalId, $chipId, 'ok', null, $data);
|
||||
|
|
@ -158,6 +164,14 @@ final class IcadService
|
|||
|
||||
// ========= HTTP =========
|
||||
|
||||
private function assertSuccess(array $response, string $message): void
|
||||
{
|
||||
$status = (int) ($response['status'] ?? 0);
|
||||
if ($status < 200 || $status >= 400) {
|
||||
throw new RuntimeException($message . ' (HTTP ' . $status . ')');
|
||||
}
|
||||
}
|
||||
|
||||
private function req(
|
||||
string $url,
|
||||
string $cookieFile,
|
||||
|
|
|
|||
|
|
@ -33,4 +33,23 @@ final class LabReferenceService
|
|||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
public static function copySpecies(string $source, string $target, ?int $userId): int
|
||||
{
|
||||
$source = SpeciesService::normalize($source);
|
||||
$target = SpeciesService::normalize($target);
|
||||
if ($source === $target || !SpeciesService::exists($source, false) || !SpeciesService::exists($target, false)) {
|
||||
throw new RuntimeException(t('lab_reference.invalid_species'));
|
||||
}
|
||||
$stmt = DB::pdo()->prepare(
|
||||
"INSERT OR IGNORE INTO lab_reference_ranges(analyzer_code,analyzer_name,species_code,parameter_code,parameter_name,unit,reference_min,reference_max,source_note,active,sort_order,updated_by,updated_at)
|
||||
SELECT analyzer_code,analyzer_name,:target,parameter_code,parameter_name,unit,reference_min,reference_max,source_note,active,sort_order,:user,datetime('now')
|
||||
FROM lab_reference_ranges WHERE species_code=:source",
|
||||
);
|
||||
$stmt->execute([':source' => $source, ':target' => $target, ':user' => $userId]);
|
||||
if ($stmt->rowCount() === 0) {
|
||||
throw new RuntimeException(t('lab_reference.nothing_copied'));
|
||||
}
|
||||
return $stmt->rowCount();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,12 +21,13 @@ final class PrivateMediaService
|
|||
return dirname(__DIR__, 2) . '/public/media/animals/' . $animalId;
|
||||
}
|
||||
|
||||
public static function ensure(string $dir): void
|
||||
public static function ensure(string $dir, bool $public = false): void
|
||||
{
|
||||
if (!is_dir($dir) && !mkdir($dir, 0700, true) && !is_dir($dir)) {
|
||||
$mode = $public ? 0755 : 0700;
|
||||
if (!is_dir($dir) && !mkdir($dir, $mode, true) && !is_dir($dir)) {
|
||||
throw new RuntimeException(t('service.image.directory_failed'));
|
||||
}
|
||||
@chmod($dir, 0700);
|
||||
@chmod($dir, $mode);
|
||||
}
|
||||
|
||||
public static function moveVisibility(int $animalId, string $filename, bool $toPublic): void
|
||||
|
|
@ -45,7 +46,7 @@ final class PrivateMediaService
|
|||
}
|
||||
throw new RuntimeException(t('service.private_media.not_found'));
|
||||
}
|
||||
self::ensure($to);
|
||||
self::ensure($to, $toPublic);
|
||||
if (!rename($source, $target)) {
|
||||
throw new RuntimeException(t('service.private_media.move_failed'));
|
||||
}
|
||||
|
|
@ -55,6 +56,19 @@ final class PrivateMediaService
|
|||
public static function migrateLegacy(): void
|
||||
{
|
||||
$db = DB::pdo();
|
||||
$public = $db
|
||||
->query('SELECT animal_id,filename FROM animal_photos WHERE is_public=1')
|
||||
->fetchAll(PDO::FETCH_ASSOC);
|
||||
foreach ($public as $row) {
|
||||
$dir = self::publicDir((int) $row['animal_id']);
|
||||
if (is_dir($dir)) {
|
||||
@chmod($dir, 0755);
|
||||
}
|
||||
$file = $dir . '/' . basename((string) $row['filename']);
|
||||
if (is_file($file)) {
|
||||
@chmod($file, 0644);
|
||||
}
|
||||
}
|
||||
$private = $db
|
||||
->query('SELECT animal_id,filename FROM animal_photos WHERE is_public=0')
|
||||
->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
|
|
|||
67
app/Services/PublicContentFormatter.php
Normal file
67
app/Services/PublicContentFormatter.php
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
final class PublicContentFormatter
|
||||
{
|
||||
public static function render(string $text): string
|
||||
{
|
||||
$lines = preg_split('/\R/u', trim($text)) ?: [];
|
||||
$html = [];
|
||||
$listOpen = false;
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '') {
|
||||
if ($listOpen) {
|
||||
$html[] = '</ul>';
|
||||
$listOpen = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/^[-*]\s+(.+)$/u', $line, $match)) {
|
||||
if (!$listOpen) {
|
||||
$html[] = '<ul>';
|
||||
$listOpen = true;
|
||||
}
|
||||
$html[] = '<li>' . self::inline($match[1]) . '</li>';
|
||||
continue;
|
||||
}
|
||||
if ($listOpen) {
|
||||
$html[] = '</ul>';
|
||||
$listOpen = false;
|
||||
}
|
||||
if (preg_match('/^(#{1,3})\s+(.+)$/u', $line, $match)) {
|
||||
$level = strlen($match[1]) + 1;
|
||||
$html[] = '<h' . $level . '>' . self::inline($match[2]) . '</h' . $level . '>';
|
||||
continue;
|
||||
}
|
||||
$html[] = '<p>' . self::inline($line) . '</p>';
|
||||
}
|
||||
if ($listOpen) {
|
||||
$html[] = '</ul>';
|
||||
}
|
||||
return implode("\n", $html);
|
||||
}
|
||||
|
||||
private static function inline(string $text): string
|
||||
{
|
||||
$links = [];
|
||||
$text = preg_replace_callback('/\[([^\]]+)\]\((https:\/\/[^\s)]+)\)/u', static function (array $match) use (&$links): string {
|
||||
if (!filter_var($match[2], FILTER_VALIDATE_URL)) {
|
||||
return $match[0];
|
||||
}
|
||||
$token = "\x1A" . count($links) . "\x1A";
|
||||
$links[] = '<a href="' . htmlspecialchars($match[2], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') .
|
||||
'" rel="noopener noreferrer">' . htmlspecialchars($match[1], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</a>';
|
||||
return $token;
|
||||
}, $text) ?? $text;
|
||||
$text = htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
$text = preg_replace('/\*\*(.+?)\*\*/u', '<strong>$1</strong>', $text) ?? $text;
|
||||
$text = preg_replace('/(?<!\*)\*([^*]+)\*(?!\*)/u', '<em>$1</em>', $text) ?? $text;
|
||||
foreach ($links as $index => $link) {
|
||||
$text = str_replace("\x1A" . $index . "\x1A", $link, $text);
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
|
|
@ -607,9 +607,14 @@ final class StatisticsService
|
|||
"SELECT COALESCE(NULLIF(TRIM(a.breed),''),'Non renseignée') label,COUNT(*) total FROM entered e JOIN animals a ON a.id=e.animal_id GROUP BY label ORDER BY total DESC,label",
|
||||
$p,
|
||||
);
|
||||
$hairTypes = $rows(
|
||||
$entered .
|
||||
"SELECT COALESCE(NULLIF(TRIM(a.hair_type),''),'Non renseigné') label,COUNT(*) total FROM entered e JOIN animals a ON a.id=e.animal_id GROUP BY label ORDER BY total DESC,label",
|
||||
$p,
|
||||
);
|
||||
$species = $rows(
|
||||
$entered .
|
||||
"SELECT CASE lower(a.species) WHEN 'cat' THEN 'Chats' WHEN 'chat' THEN 'Chats' WHEN 'dog' THEN 'Chiens' WHEN 'chien' THEN 'Chiens' ELSE COALESCE(NULLIF(TRIM(a.species),''),'Inconnue') END label,COUNT(*) total FROM entered e JOIN animals a ON a.id=e.animal_id GROUP BY label ORDER BY total DESC",
|
||||
"SELECT CASE lower(a.species) WHEN 'cat' THEN 'chat' WHEN 'dog' THEN 'chien' ELSE COALESCE(NULLIF(TRIM(lower(a.species)),''),'unknown') END label,COUNT(*) total FROM entered e JOIN animals a ON a.id=e.animal_id GROUP BY label ORDER BY total DESC",
|
||||
$p,
|
||||
);
|
||||
$intakeTypes = $rows(
|
||||
|
|
@ -680,6 +685,7 @@ final class StatisticsService
|
|||
return [
|
||||
'colors' => $colors,
|
||||
'breeds' => $breeds,
|
||||
'hairTypes' => $hairTypes,
|
||||
'species' => $species,
|
||||
'intakeTypes' => $intakeTypes,
|
||||
'intakeReasons' => $intakeReasons,
|
||||
|
|
|
|||
Loading…
Reference in a new issue