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
This commit is contained in:
360
core/SessionManager.php
Normal file
360
core/SessionManager.php
Normal file
@@ -0,0 +1,360 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user