HEX
Server: Apache
System: Linux info 3.0 #1337 SMP Tue Jan 01 00:00:00 CEST 2000 all GNU/Linux
User: u115237990 (7145895)
PHP: 8.3.32
Disabled: NONE
Upload Files
File: /homepages/35/d993672390/htdocs/dermiteseborrheique/includes/functions.php
<?php
/**
 * functions.php — Fonctions utilitaires globales
 */

declare(strict_types=1);

if (!defined('DS_APP')) die('Accès direct interdit.');

// ── Sécurité & Sanitisation ───────────────────────────────────────────────────

/**
 * Échappe une valeur pour affichage HTML sécurisé
 */
function e(string $value): string {
    return htmlspecialchars($value, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}

/**
 * Génère un UUID v4
 */
function generate_uuid(): string {
    $data = random_bytes(16);
    $data[6] = chr(ord($data[6]) & 0x0f | 0x40);
    $data[8] = chr(ord($data[8]) & 0x3f | 0x80);
    return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}

/**
 * Génère un token sécurisé aléatoire
 */
function generate_token(int $bytes = 32): string {
    return bin2hex(random_bytes($bytes));
}

/**
 * Hash d'un token pour stockage en BDD (ne jamais stocker le token brut)
 */
function hash_token(string $token): string {
    return hash('sha256', $token);
}

/**
 * Redirige vers une URL et arrête l'exécution
 */
function redirect(string $path, int $code = 302): never {
    $url = str_starts_with($path, 'http') ? $path : APP_URL . '/' . ltrim($path, '/');
    header("Location: $url", true, $code);
    exit;
}

/**
 * Retourne l'IP réelle du visiteur (compatible Cloudflare / proxy)
 */
function get_client_ip(): string {
    $keys = ['HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', 'REMOTE_ADDR'];
    foreach ($keys as $key) {
        if (!empty($_SERVER[$key])) {
            $ip = trim(explode(',', $_SERVER[$key])[0]);
            if (filter_var($ip, FILTER_VALIDATE_IP)) return $ip;
        }
    }
    return '0.0.0.0';
}

/**
 * Nettoie et valide une adresse email
 */
function sanitize_email(string $email): string|false {
    $email = strtolower(trim($email));
    return filter_var($email, FILTER_VALIDATE_EMAIL) ? $email : false;
}

/**
 * Nettoie une chaîne de caractères basique
 */
function sanitize_string(string $str, int $maxLength = 255): string {
    return mb_substr(trim(strip_tags($str)), 0, $maxLength);
}

// ── CSRF Protection ───────────────────────────────────────────────────────────

/**
 * Génère (ou récupère) le token CSRF de la session courante
 */
function csrf_token(): string {
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = generate_token(32);
    }
    return $_SESSION['csrf_token'];
}

/**
 * Génère le champ HTML hidden CSRF à insérer dans les formulaires
 */
function csrf_field(): string {
    return '<input type="hidden" name="_csrf" value="' . e(csrf_token()) . '">';
}

/**
 * Vérifie le token CSRF — stoppe l'exécution si invalide
 */
function csrf_verify(): void {
    $token = $_POST['_csrf'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
    if (!hash_equals(csrf_token(), $token)) {
        http_response_code(403);
        die('Token CSRF invalide. Rechargez la page et réessayez.');
    }
}

// ── Flash Messages ────────────────────────────────────────────────────────────

/**
 * Stocke un message flash (affiché une seule fois)
 * Types : success | error | warning | info
 */
function flash(string $type, string $message): void {
    $_SESSION['flash'][] = ['type' => $type, 'message' => $message];
}

/**
 * Récupère et vide les messages flash
 */
function get_flash(): array {
    $messages = $_SESSION['flash'] ?? [];
    unset($_SESSION['flash']);
    return $messages;
}

/**
 * Affiche les messages flash en HTML
 */
function render_flash(): string {
    $messages = get_flash();
    if (empty($messages)) return '';

    $html = '<div class="flash-container">';
    foreach ($messages as $msg) {
        $type = e($msg['type']);
        $text = e($msg['message']);
        $html .= "<div class=\"flash flash-{$type}\">{$text}</div>";
    }
    return $html . '</div>';
}

// ── Dates & Temps ─────────────────────────────────────────────────────────────

/**
 * Date au format MySQL (YYYY-MM-DD HH:MM:SS)
 */
function now(): string {
    return date('Y-m-d H:i:s');
}

/**
 * Date future (ex: dans 30 jours)
 */
function future_date(int $days): string {
    return date('Y-m-d H:i:s', strtotime("+{$days} days"));
}

/**
 * Formate une date pour affichage humain
 */
function format_date(string $date, string $format = 'd/m/Y'): string {
    return date($format, strtotime($date));
}

/**
 * Temps relatif (il y a X minutes, jours...)
 */
function time_ago(string $datetime): string {
    $diff = time() - strtotime($datetime);

    return match(true) {
        $diff < 60     => 'À l\'instant',
        $diff < 3600   => round($diff / 60) . ' min',
        $diff < 86400  => round($diff / 3600) . 'h',
        $diff < 604800 => round($diff / 86400) . 'j',
        default        => format_date($datetime, 'd/m/Y'),
    };
}

// ── Internationalisation (i18n) ───────────────────────────────────────────────

$GLOBALS['_lang'] = [];

/**
 * Charge les traductions pour la langue donnée
 */
function load_lang(string $lang = DEFAULT_LANG): void {
    $file = DS_LANG . $lang . '.php';
    if (!file_exists($file)) {
        $file = DS_LANG . DEFAULT_LANG . '.php'; // Fallback
    }
    $GLOBALS['_lang'] = require $file;
    $GLOBALS['_current_lang'] = $lang;
}

/**
 * Traduit une clé
 * Usage : __('auth.login_title') → "Connexion"
 * Supporte les placeholders : __('welcome', ['name' => 'Marie']) → "Bonjour Marie"
 */
function __(string $key, array $replace = []): string {
    $parts = explode('.', $key);
    $value = $GLOBALS['_lang'];

    foreach ($parts as $part) {
        $value = $value[$part] ?? null;
        if ($value === null) return $key; // Clé manquante = retourne la clé
    }

    if (!is_string($value)) return $key;

    foreach ($replace as $k => $v) {
        $value = str_replace(':' . $k, (string)$v, $value);
    }

    return $value;
}

/**
 * Traduit et échappe pour HTML
 */
function _e(string $key, array $replace = []): string {
    return e(__($key, $replace));
}

/**
 * Retourne la langue courante
 */
function current_lang(): string {
    return $GLOBALS['_current_lang'] ?? DEFAULT_LANG;
}

// ── Validation ────────────────────────────────────────────────────────────────

/**
 * Classe simple de validation
 */
class Validator {
    private array $errors = [];
    private array $data;

    public function __construct(array $data) {
        $this->data = $data;
    }

    public function required(string $field, string $label = ''): static {
        if (empty(trim((string)($this->data[$field] ?? '')))) {
            $this->errors[$field] = ($label ?: $field) . ' est requis.';
        }
        return $this;
    }

    public function email(string $field): static {
        if (!empty($this->data[$field]) && !filter_var($this->data[$field], FILTER_VALIDATE_EMAIL)) {
            $this->errors[$field] = 'Adresse email invalide.';
        }
        return $this;
    }

    public function min(string $field, int $min, string $label = ''): static {
        $val = $this->data[$field] ?? '';
        if (mb_strlen((string)$val) < $min) {
            $this->errors[$field] = ($label ?: $field) . " doit contenir au moins {$min} caractères.";
        }
        return $this;
    }

    public function max(string $field, int $max, string $label = ''): static {
        $val = $this->data[$field] ?? '';
        if (mb_strlen((string)$val) > $max) {
            $this->errors[$field] = ($label ?: $field) . " ne peut dépasser {$max} caractères.";
        }
        return $this;
    }

    public function matches(string $field, string $otherField, string $label = ''): static {
        if (($this->data[$field] ?? '') !== ($this->data[$otherField] ?? '')) {
            $this->errors[$field] = ($label ?: 'Les champs') . ' ne correspondent pas.';
        }
        return $this;
    }

    public function fails(): bool { return !empty($this->errors); }
    public function passes(): bool { return empty($this->errors); }
    public function errors(): array { return $this->errors; }
    public function first(string $field): string { return $this->errors[$field] ?? ''; }
    public function get(string $field, mixed $default = null): mixed { return $this->data[$field] ?? $default; }
}

// ── Helpers divers ────────────────────────────────────────────────────────────

/**
 * Retourne la valeur d'une variable POST nettoyée
 */
function post(string $key, mixed $default = ''): mixed {
    return $_POST[$key] ?? $default;
}

/**
 * Retourne la valeur d'une variable GET nettoyée
 */
function get_param(string $key, mixed $default = ''): mixed {
    return $_GET[$key] ?? $default;
}

/**
 * Vérifie si la requête est une requête AJAX
 */
function is_ajax(): bool {
    return !empty($_SERVER['HTTP_X_REQUESTED_WITH'])
        && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
}

/**
 * Envoie une réponse JSON et arrête l'exécution
 */
function json_response(array $data, int $status = 200): never {
    http_response_code($status);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
    exit;
}

/**
 * Génère une URL absolue
 */
function url(string $path = ''): string {
    return APP_URL . '/' . ltrim($path, '/');
}

/**
 * Génère le chemin vers un asset (css, js, img)
 */
function asset(string $path): string {
    return url('assets/' . ltrim($path, '/'));
}

/**
 * Retourne le prénom d'un utilisateur
 * Gère le cas Google qui retourne parfois "NOM Prénom"
 */
function user_firstname(array $user): string {
    $name  = trim($user['name'] ?? '');
    if (empty($name)) return 'Utilisateur';
    $parts = array_filter(explode(' ', $name));
    $parts = array_values($parts);
    if (count($parts) === 1) return $parts[0];
    // Détecter si le premier mot est tout en majuscules (= NOM de famille)
    // Ex: "DELGADO Adrien" → retourner "Adrien"
    if (mb_strtoupper($parts[0]) === $parts[0] && mb_strlen($parts[0]) > 1) {
        return end($parts);
    }
    // Connexion Google : parfois format "Nom Prénom" avec première lettre maj seulement
    // On prend le premier mot par défaut
    return $parts[0];
}