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];
}