203 lines
5.7 KiB
PHP
203 lines
5.7 KiB
PHP
<?php
|
||
declare (strict_types = 1);
|
||
|
||
// Interdit l’accès direct au fichier (pas de HTML ici)
|
||
if (isset($_SERVER['SCRIPT_FILENAME']) && realpath(__FILE__) === realpath($_SERVER['SCRIPT_FILENAME'])) {
|
||
http_response_code(403);
|
||
exit('Forbidden');
|
||
}
|
||
|
||
/** Est-ce que l'utilisateur est connecté ? */
|
||
function is_logged_in(): bool
|
||
{
|
||
return isset($_SESSION['compte_id'], $_SESSION['reference_id'])
|
||
&& $_SESSION['compte_id'] !== ''
|
||
&& $_SESSION['reference_id'] !== '';
|
||
}
|
||
|
||
/** Timeout inactivité (2h) */
|
||
function enforce_session_timeout(int $maxIdleSeconds = 7200): void
|
||
{
|
||
$now = time();
|
||
$last = (int) ($_SESSION['last_activity'] ?? 0);
|
||
|
||
if ($last > 0 && ($now - $last) > $maxIdleSeconds) {
|
||
logout_and_redirect_to_login($_SERVER['REQUEST_URI'] ?? '/');
|
||
}
|
||
$_SESSION['last_activity'] = $now;
|
||
}
|
||
|
||
/** Anti open-redirect : n’autorise que les chemins internes commençant par '/' */
|
||
function next_safe(?string $u): ?string
|
||
{
|
||
if (! $u) {
|
||
return null;
|
||
}
|
||
|
||
if (! preg_match('#^/[^\\r\\n]*$#', $u)) {
|
||
return null;
|
||
}
|
||
|
||
if (strpos($u, '//') === 0) {
|
||
return null;
|
||
}
|
||
|
||
return $u;
|
||
}
|
||
|
||
function redirect_forbidden(?string $requestUri = null): void
|
||
{
|
||
$u = $requestUri ?: ($_SERVER['REQUEST_URI'] ?? '/');
|
||
$u = next_safe($u) ?? '/';
|
||
header('Location: /403.php?next=' . rawurlencode($u), true, 303);
|
||
exit;
|
||
}
|
||
|
||
function redirect_to_login_with_next(?string $requestUri = null): void
|
||
{
|
||
$u = $requestUri ?: ($_SERVER['REQUEST_URI'] ?? '/');
|
||
$u = next_safe($u) ?? '/';
|
||
|
||
$_SESSION['redirect_to'] = $u;
|
||
header('Location: /connexion.php?next=' . rawurlencode($u), true, 303);
|
||
exit;
|
||
}
|
||
|
||
function logout_and_redirect_to_login(): void
|
||
{
|
||
$_SESSION = [];
|
||
|
||
if (session_status() === PHP_SESSION_ACTIVE) {
|
||
$params = session_get_cookie_params();
|
||
|
||
setcookie(session_name(), '', [
|
||
'expires' => time() - 42000,
|
||
'path' => $params['path'] ?? '/',
|
||
'domain' => $params['domain'] ?? '',
|
||
'secure' => (bool) ($params['secure'] ?? false),
|
||
'httponly' => (bool) ($params['httponly'] ?? true),
|
||
'samesite' => $params['samesite'] ?? 'Lax',
|
||
]);
|
||
|
||
session_destroy();
|
||
}
|
||
|
||
header('Location: /index.php', true, 303);
|
||
exit;
|
||
}
|
||
|
||
/**
|
||
* 🔒 Re-check rôle depuis DB (optionnel) + hydrate $_SESSION['role'] si absent.
|
||
* Retourne le rôle (string) ou '' si introuvable.
|
||
*/
|
||
function refresh_role_from_db(PDO $pdo): string
|
||
{
|
||
$compteId = (int) ($_SESSION['compte_id'] ?? 0);
|
||
if ($compteId <= 0) {
|
||
return '';
|
||
}
|
||
|
||
$stmt = $pdo->prepare("SELECT role FROM compte WHERE id=? LIMIT 1");
|
||
$stmt->execute([$compteId]);
|
||
$role = (string) ($stmt->fetchColumn() ?: '');
|
||
if ($role !== '') {
|
||
$_SESSION['role'] = $role;
|
||
}
|
||
return strtolower($role);
|
||
}
|
||
|
||
/**
|
||
* ✅ À appeler en haut des pages protégées.
|
||
* - $allowedRoles = null => tous rôles
|
||
* - options:
|
||
* - 'pdo' => PDO (pour re-check DB si demandé)
|
||
* - 'recheck_db_role' => true/false
|
||
* - 'admin_strict_reference' => int|null (ex: 88888888)
|
||
*/
|
||
function require_login_or_redirect(?array $allowedRoles = null, array $options = []): void
|
||
{
|
||
if (! is_logged_in()) {
|
||
redirect_to_login_with_next($_SERVER['REQUEST_URI'] ?? '/');
|
||
}
|
||
|
||
enforce_session_timeout(7200);
|
||
|
||
$pdo = $options['pdo'] ?? ($GLOBALS['pdo'] ?? null);
|
||
$recheck = (bool) ($options['recheck_db_role'] ?? false);
|
||
|
||
// Rôle: session d'abord, DB si demandé (ou si role manquant)
|
||
$userRole = strtolower((string) ($_SESSION['role'] ?? ''));
|
||
if (($userRole === '' || $recheck) && ($pdo instanceof PDO)) {
|
||
$userRole = refresh_role_from_db($pdo);
|
||
}
|
||
|
||
if ($allowedRoles) {
|
||
$allowed = array_map(static fn($r) => strtolower((string) $r), $allowedRoles);
|
||
|
||
if (! in_array($userRole, $allowed, true)) {
|
||
redirect_forbidden($_SERVER['REQUEST_URI'] ?? '/');
|
||
}
|
||
|
||
// 🔥 Renfort admin optionnel : role + reference_id
|
||
if (in_array('admin', $allowed, true)) {
|
||
$strictRef = $options['admin_strict_reference'] ?? null;
|
||
if ($strictRef !== null) {
|
||
$refId = (int) ($_SESSION['reference_id'] ?? 0);
|
||
if ($refId !== (int) $strictRef) {
|
||
redirect_forbidden($_SERVER['REQUEST_URI'] ?? '/');
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* =========================
|
||
Admin helpers (CSRF + logs)
|
||
========================= */
|
||
|
||
/** Token CSRF session */
|
||
function ensure_csrf_token(): string
|
||
{
|
||
if (empty($_SESSION['csrf_token'])) {
|
||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||
}
|
||
return (string) $_SESSION['csrf_token'];
|
||
}
|
||
|
||
/** Exige POST + CSRF (supporte aussi header X-CSRF-Token si tu veux fetch/AJAX) */
|
||
function require_post_csrf(): void
|
||
{
|
||
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
|
||
http_response_code(405);
|
||
exit('Méthode non autorisée');
|
||
}
|
||
|
||
$token = $_POST['csrf_token'] ?? '';
|
||
if ($token === '' && ! empty($_SERVER['HTTP_X_CSRF_TOKEN'])) {
|
||
$token = (string) $_SERVER['HTTP_X_CSRF_TOKEN'];
|
||
}
|
||
|
||
if ($token === '' || ! hash_equals((string) ($_SESSION['csrf_token'] ?? ''), (string) $token)) {
|
||
http_response_code(419);
|
||
exit('CSRF invalide');
|
||
}
|
||
}
|
||
|
||
/** Log admin minimal */
|
||
function log_admin_action(PDO $pdo, int $userId, string $action, string $details = ''): void
|
||
{
|
||
// limite simple pour éviter d'exploser la table
|
||
if (strlen($details) > 2000) {
|
||
$details = substr($details, 0, 2000);
|
||
}
|
||
|
||
$pdo->prepare(
|
||
"INSERT INTO admin_actions_log (user_id, action, details, ip) VALUES (?,?,?,?)"
|
||
)->execute([
|
||
$userId,
|
||
$action,
|
||
$details,
|
||
$_SERVER['REMOTE_ADDR'] ?? '',
|
||
]);
|
||
}
|