Files
fuzip/core/SessionManager.php
Charles bd6d321ed7 Initial commit: FuZip - Application de fusion interactive de fichiers ZIP
- Backend PHP: architecture MVC avec API REST (upload, merge, preview, extract)
  - Frontend JavaScript: composants modulaires (arborescence, upload, themes, i18n)
  - Fonctionnalités: drag&drop, sélection exclusive, détection conflits, persistance état
  - Sécurité: validation stricte, isolation sessions, sanitization chemins
  - UI/UX: responsive, thèmes clair/sombre, multi-langue (FR/EN)
  - Documentation: README complet avec installation et utilisation
2026-01-12 03:29:01 +01:00

361 lines
12 KiB
PHP

<?php
/**
* SessionManager - Gestion des sessions et des dossiers d'upload
*
* Cette classe gère :
* - L'initialisation des sessions PHP
* - La création des dossiers d'upload par session
* - Le tracking de l'activité (last_access.txt)
* - Le nettoyage automatique des sessions expirées (> 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;
}
}
}