24h) */ require_once __DIR__ . '/Config.php'; class SessionManager { /** * Nom du fichier pour tracker la dernière activité */ const LAST_ACCESS_FILE = 'last_access.txt'; /** * Initialise une session PHP et crée le dossier d'upload associé * * @return string L'ID de session * @throws Exception Si la création du dossier échoue */ public static function init(): string { // Démarrer la session si pas déjà démarrée if (session_status() === PHP_SESSION_NONE) { // Vérifier si on est en CLI (pour les tests) if (php_sapi_name() === 'cli') { // En CLI, générer un ID de session unique pour le test $sessionId = 'cli_test_' . bin2hex(random_bytes(16)); Config::log("Mode CLI détecté, session ID générée : {$sessionId}"); } else { // Mode web normal session_start(); $sessionId = session_id(); Config::log("Session initialisée : {$sessionId}"); } } else { $sessionId = session_id(); Config::log("Session existante : {$sessionId}"); } // Vérifier que le session_id n'est pas vide if (empty($sessionId)) { Config::log("Session ID vide, génération d'un ID unique", 'WARNING'); $sessionId = 'fallback_' . bin2hex(random_bytes(16)); } // Créer le dossier d'upload pour cette session $uploadDir = self::getUploadDir($sessionId); if (!file_exists($uploadDir)) { if (!mkdir($uploadDir, 0755, true)) { Config::log("Échec création dossier : {$uploadDir}", 'ERROR'); throw new Exception("Impossible de créer le dossier de session"); } Config::log("Dossier de session créé : {$uploadDir}"); } // Mettre à jour le timestamp d'accès self::updateAccess($sessionId); // Nettoyer les anciennes sessions (appelé à chaque initialisation) self::cleanOldSessions(); return $sessionId; } /** * Obtient le chemin du dossier d'upload pour une session donnée * * @param string $sessionId ID de la session * @return string Chemin absolu vers uploads/{sessionId}/ */ public static function getUploadDir(string $sessionId): string { // Sécurité : valider que le session_id ne contient pas de caractères dangereux // Autorise lettres, chiffres et underscores (pour cli_test_ et fallback_) if (!preg_match('/^[a-zA-Z0-9_]{10,150}$/', $sessionId)) { Config::log("Session ID invalide : {$sessionId}", 'WARNING'); throw new InvalidArgumentException("Session ID invalide"); } // Vérifier qu'il n'y a pas de .. ou de chemins relatifs if (strpos($sessionId, '..') !== false || strpos($sessionId, '/') !== false || strpos($sessionId, '\\') !== false) { Config::log("Session ID contient des caractères dangereux : {$sessionId}", 'WARNING'); throw new InvalidArgumentException("Session ID invalide"); } $uploadDir = Config::getUploadDir() . $sessionId . DIRECTORY_SEPARATOR; return $uploadDir; } /** * Met à jour le timestamp de dernière activité pour une session * * @param string $sessionId ID de la session * @return bool True si succès, false sinon */ public static function updateAccess(string $sessionId): bool { try { $uploadDir = self::getUploadDir($sessionId); $lastAccessFile = $uploadDir . self::LAST_ACCESS_FILE; $timestamp = time(); $result = file_put_contents($lastAccessFile, $timestamp); if ($result !== false) { Config::log("Timestamp mis à jour pour session {$sessionId} : {$timestamp}"); return true; } Config::log("Échec mise à jour timestamp pour session {$sessionId}", 'WARNING'); return false; } catch (Exception $e) { Config::log("Erreur updateAccess : " . $e->getMessage(), 'ERROR'); return false; } } /** * Obtient le timestamp de dernière activité d'une session * * @param string $sessionId ID de la session * @return int|null Timestamp ou null si non trouvé */ public static function getLastAccess(string $sessionId): ?int { try { $uploadDir = self::getUploadDir($sessionId); $lastAccessFile = $uploadDir . self::LAST_ACCESS_FILE; if (!file_exists($lastAccessFile)) { return null; } $content = file_get_contents($lastAccessFile); return $content !== false ? (int)$content : null; } catch (Exception $e) { Config::log("Erreur getLastAccess : " . $e->getMessage(), 'ERROR'); return null; } } /** * Nettoie les sessions expirées (> SESSION_LIFETIME) * Supprime récursivement les dossiers et leur contenu * * @return int Nombre de sessions supprimées */ public static function cleanOldSessions(): int { $deletedCount = 0; $uploadBaseDir = Config::getUploadDir(); Config::log("Début nettoyage sessions expirées"); try { // Parcourir tous les dossiers dans uploads/ if (!is_dir($uploadBaseDir)) { Config::log("Dossier uploads inexistant : {$uploadBaseDir}", 'WARNING'); return 0; } $sessionDirs = scandir($uploadBaseDir); foreach ($sessionDirs as $sessionId) { // Ignorer . et .. if ($sessionId === '.' || $sessionId === '..') { continue; } $sessionPath = $uploadBaseDir . $sessionId; // Vérifier que c'est un dossier if (!is_dir($sessionPath)) { continue; } // Récupérer le timestamp de dernière activité $lastAccess = self::getLastAccess($sessionId); if ($lastAccess === null) { // Pas de fichier last_access.txt, utiliser mtime du dossier $lastAccess = filemtime($sessionPath); } $age = time() - $lastAccess; // Si la session a expiré if ($age > Config::SESSION_LIFETIME) { Config::log("Session expirée : {$sessionId} (âge: {$age}s)"); if (self::deleteDirectory($sessionPath)) { $deletedCount++; Config::log("Session supprimée : {$sessionId}"); } else { Config::log("Échec suppression session : {$sessionId}", 'ERROR'); } } } if ($deletedCount > 0) { Config::log("Nettoyage terminé : {$deletedCount} session(s) supprimée(s)"); } } catch (Exception $e) { Config::log("Erreur lors du nettoyage : " . $e->getMessage(), 'ERROR'); } return $deletedCount; } /** * Supprime récursivement un dossier et son contenu * * @param string $dir Chemin du dossier à supprimer * @return bool True si succès, false sinon */ private static function deleteDirectory(string $dir): bool { if (!file_exists($dir)) { return true; } if (!is_dir($dir)) { return unlink($dir); } // Parcourir le contenu du dossier $items = scandir($dir); foreach ($items as $item) { if ($item === '.' || $item === '..') { continue; } $itemPath = $dir . DIRECTORY_SEPARATOR . $item; if (is_dir($itemPath)) { // Récursion pour sous-dossiers if (!self::deleteDirectory($itemPath)) { return false; } } else { // Supprimer fichier if (!unlink($itemPath)) { Config::log("Échec suppression fichier : {$itemPath}", 'WARNING'); return false; } } } // Supprimer le dossier vide return rmdir($dir); } /** * Supprime manuellement une session (ex: bouton "Nouveau") * * @param string $sessionId ID de la session à supprimer * @return bool True si succès */ public static function deleteSession(string $sessionId): bool { try { $uploadDir = self::getUploadDir($sessionId); if (!file_exists($uploadDir)) { return true; // Déjà supprimé } $result = self::deleteDirectory($uploadDir); if ($result) { Config::log("Session supprimée manuellement : {$sessionId}"); } else { Config::log("Échec suppression manuelle session : {$sessionId}", 'ERROR'); } return $result; } catch (Exception $e) { Config::log("Erreur deleteSession : " . $e->getMessage(), 'ERROR'); return false; } } /** * Vérifie si une session existe et est valide * * @param string $sessionId ID de la session * @return bool True si la session existe et n'est pas expirée */ public static function isSessionValid(string $sessionId): bool { try { $uploadDir = self::getUploadDir($sessionId); if (!file_exists($uploadDir)) { return false; } $lastAccess = self::getLastAccess($sessionId); if ($lastAccess === null) { return false; } $age = time() - $lastAccess; return $age <= Config::SESSION_LIFETIME; } catch (Exception $e) { Config::log("Erreur isSessionValid : " . $e->getMessage(), 'ERROR'); return false; } } /** * Obtient les statistiques d'une session * * @param string $sessionId ID de la session * @return array|null Tableau avec infos ou null si erreur */ public static function getSessionStats(string $sessionId): ?array { try { $uploadDir = self::getUploadDir($sessionId); if (!file_exists($uploadDir)) { return null; } $lastAccess = self::getLastAccess($sessionId); $age = $lastAccess ? time() - $lastAccess : null; // Compter les fichiers dans le dossier $files = glob($uploadDir . '*'); $fileCount = count($files); // Calculer la taille totale $totalSize = 0; foreach ($files as $file) { if (is_file($file)) { $totalSize += filesize($file); } } return [ 'session_id' => $sessionId, 'upload_dir' => $uploadDir, 'last_access' => $lastAccess, 'age_seconds' => $age, 'file_count' => $fileCount, 'total_size' => $totalSize, 'total_size_formatted' => Config::formatBytes($totalSize), 'is_expired' => $age ? $age > Config::SESSION_LIFETIME : false ]; } catch (Exception $e) { Config::log("Erreur getSessionStats : " . $e->getMessage(), 'ERROR'); return null; } } }