211 lines
5.4 KiB
PHP
211 lines
5.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
function app_timezone(): string
|
|
{
|
|
$timezone = trim((string) Base::instance()->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);
|
|
}
|