get('app.timezone')); return ($timezone !== '' && in_array($timezone, DateTimeZone::listIdentifiers(), true)) ? $timezone : 'UTC'; } function app_now(): string { return gmdate('Y-m-d H:i:s'); } function app_is_prod(): bool { return Base::instance()->get('app.env') === 'prod'; } function app_ensure_dir(string $path): void { if (!is_dir($path)) { mkdir($path, 0775, true); } } function app_unique_slug(string $value, callable $exists): string { $base = Web::instance()->slug(trim($value)); if ($base === '') { $base = 'article'; } if (!$exists($base)) { return $base; } for ($i = 2; $i <= 1000; $i++) { $candidate = $base . '-' . $i; if (!$exists($candidate)) { return $candidate; } } throw new RuntimeException('Impossible de générer un slug unique.'); } function app_format_datetime_fr(string $value): string { static $utc, $formatter; $value = trim($value); if ($value === '') { return ''; } try { $utc ??= new DateTimeZone('UTC'); $date = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $value, $utc); if (!$date instanceof DateTimeImmutable) { $date = new DateTimeImmutable($value, $utc); } $date = $date->setTimezone(new DateTimeZone(date_default_timezone_get())); $formatter ??= new IntlDateFormatter( 'fr_FR', IntlDateFormatter::LONG, IntlDateFormatter::SHORT, date_default_timezone_get(), IntlDateFormatter::GREGORIAN, "d MMMM yyyy 'à' HH:mm" ); $formatted = $formatter->format($date); return is_string($formatted) && $formatted !== '' ? $formatted : $value; } catch (Throwable) { return $value; } } function app_trusted_proxies(): array { $value = Base::instance()->get('app.trusted_proxies'); if (is_array($value)) { $items = []; array_walk_recursive($value, static function (mixed $item) use (&$items): void { if (is_string($item) || is_numeric($item)) { $items[] = (string) $item; } }); } else { $raw = trim((string) $value); if ($raw === '') { return []; } $items = preg_split('/[\s,]+/', $raw) ?: []; } $normalized = []; foreach ($items as $item) { foreach (preg_split('/[\s,]+/', trim((string) $item)) ?: [] as $part) { $part = trim($part); if ($part !== '') { $normalized[] = $part; } } } return array_values(array_unique($normalized)); } function app_is_trusted_proxy(?string $ip = null): bool { $ip = trim((string) ($ip ?? Base::instance()->get('SERVER.REMOTE_ADDR'))); if ($ip === '' || filter_var($ip, FILTER_VALIDATE_IP) === false) { return false; } foreach (app_trusted_proxies() as $proxy) { if (app_ip_matches_proxy($ip, $proxy)) { return true; } } return false; } function app_request_scheme(): string { $f3 = Base::instance(); $https = strtolower(trim((string) $f3->get('SERVER.HTTPS'))); if ($https !== '' && $https !== 'off' && $https !== '0') { return 'https'; } $scheme = strtolower(trim((string) $f3->get('SCHEME'))); if ($scheme === 'https') { return 'https'; } // Derrière un reverse proxy de confiance, on accepte le proto transmis. if (app_is_trusted_proxy()) { $forwardedProto = trim((string) $f3->get('SERVER.HTTP_X_FORWARDED_PROTO')); if ($forwardedProto !== '') { $forwardedProto = strtolower(trim(explode(',', $forwardedProto)[0])); return $forwardedProto === 'https' ? 'https' : 'http'; } $forwarded = trim((string) $f3->get('SERVER.HTTP_FORWARDED')); if ($forwarded !== '' && preg_match('/(?:^|[;,]\s*)proto=(https?)/i', $forwarded, $matches) === 1) { return strtolower($matches[1]) === 'https' ? 'https' : 'http'; } } return 'http'; } function app_ip_matches_proxy(string $ip, string $proxy): bool { $proxy = trim($proxy); if ($proxy === '') { return false; } if (!str_contains($proxy, '/')) { return filter_var($proxy, FILTER_VALIDATE_IP) !== false && strcasecmp($ip, $proxy) === 0; } [$subnet, $prefix] = explode('/', $proxy, 2); $subnet = trim($subnet); $prefix = trim($prefix); $ipBinary = inet_pton($ip); $subnetBinary = inet_pton($subnet); if ($ipBinary === false || $subnetBinary === false || strlen($ipBinary) !== strlen($subnetBinary)) { return false; } if (!ctype_digit($prefix)) { return false; } $prefixLength = (int) $prefix; $maxBits = strlen($ipBinary) * 8; if ($prefixLength < 0 || $prefixLength > $maxBits) { return false; } $fullBytes = intdiv($prefixLength, 8); if ($fullBytes > 0 && substr($ipBinary, 0, $fullBytes) !== substr($subnetBinary, 0, $fullBytes)) { return false; } $remainingBits = $prefixLength % 8; if ($remainingBits === 0) { return true; } $mask = (0xFF << (8 - $remainingBits)) & 0xFF; return (ord($ipBinary[$fullBytes]) & $mask) === (ord($subnetBinary[$fullBytes]) & $mask); }