nano-pro/session_manager.php
DJP 7d1cd03a32 Add multi-user support with 24-hour image expiration
Implemented complete session management system for multiple users:

**New Features:**
- Isolated user sessions with unique session IDs
- File-based image storage (not in PHP session)
- Automatic 24-hour image expiration
- Session directories: uploads/sessions/{session_id}/
- Images stored with metadata (creation time, expiry, MIME type)

**New Files:**
- session_manager.php - Complete session management class
- cleanup.php - Cron script to delete expired images
- uploads/.htaccess - Security: prevent direct file access

**Updated Files:**
- api.php - Uses SessionManager for file-based storage
- index.php - Loads images from disk via SessionManager
- .gitignore - Exclude user uploads from repository

**Usage:**
- Each user gets isolated session automatically
- Images auto-delete after 24 hours
- Run cleanup.php via cron: `0 * * * * php cleanup.php`

**Security:**
- Session IDs regenerated on first access
- Upload directory protected by .htaccess
- User images isolated by session

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
2025-12-16 08:56:26 -05:00

314 lines
9 KiB
PHP

<?php
/**
* Session Manager for Multi-User Support
* Handles isolated sessions, file-based image storage, and automatic cleanup
*/
class SessionManager {
private $sessionId;
private $uploadDir;
private $sessionDir;
public function __construct() {
// Configure session for better security
ini_set('session.cookie_httponly', 1);
ini_set('session.use_only_cookies', 1);
ini_set('session.cookie_lifetime', 86400); // 24 hours
ini_set('session.gc_maxlifetime', 86400); // 24 hours
// Start session if not already started
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Regenerate session ID on first access for security
if (!isset($_SESSION['initialized'])) {
session_regenerate_id(true);
$_SESSION['initialized'] = true;
$_SESSION['created_at'] = time();
}
$this->sessionId = session_id();
$this->uploadDir = __DIR__ . '/uploads/sessions';
$this->sessionDir = $this->uploadDir . '/' . $this->sessionId;
// Create session directory if it doesn't exist
if (!is_dir($this->sessionDir)) {
mkdir($this->sessionDir, 0755, true);
mkdir($this->sessionDir . '/images', 0755, true);
}
// Initialize session data
$this->initializeSessionData();
}
private function initializeSessionData() {
if (!isset($_SESSION['conversation_history'])) {
$_SESSION['conversation_history'] = [];
}
if (!isset($_SESSION['image_history'])) {
$_SESSION['image_history'] = [];
}
if (!isset($_SESSION['current_image_path'])) {
$_SESSION['current_image_path'] = null;
}
}
public function getSessionId() {
return $this->sessionId;
}
public function getSessionDir() {
return $this->sessionDir;
}
public function getImagesDir() {
return $this->sessionDir . '/images';
}
/**
* Save image to disk and return file path
*/
public function saveImage($imageData, $mimeType = 'image/png') {
$extension = $this->getExtensionFromMime($mimeType);
$timestamp = time();
$filename = 'image_' . $timestamp . '_' . uniqid() . '.' . $extension;
$filepath = $this->getImagesDir() . '/' . $filename;
// Decode base64 if needed
if (base64_decode($imageData, true) !== false) {
$imageData = base64_decode($imageData);
}
// Save image
if (file_put_contents($filepath, $imageData) === false) {
throw new Exception('Failed to save image to disk');
}
// Save metadata
$metadataFile = $filepath . '.meta';
$metadata = [
'created_at' => $timestamp,
'mime_type' => $mimeType,
'session_id' => $this->sessionId,
'expires_at' => $timestamp + 86400 // 24 hours
];
file_put_contents($metadataFile, json_encode($metadata));
return $filename;
}
/**
* Get image data from disk
*/
public function getImage($filename) {
$filepath = $this->getImagesDir() . '/' . $filename;
if (!file_exists($filepath)) {
return null;
}
// Check if image has expired
$metadataFile = $filepath . '.meta';
if (file_exists($metadataFile)) {
$metadata = json_decode(file_get_contents($metadataFile), true);
if ($metadata && $metadata['expires_at'] < time()) {
// Image expired, delete it
$this->deleteImage($filename);
return null;
}
}
return [
'data' => base64_encode(file_get_contents($filepath)),
'mime_type' => $this->getMimeTypeFromFile($filepath)
];
}
/**
* Delete image from disk
*/
public function deleteImage($filename) {
$filepath = $this->getImagesDir() . '/' . $filename;
$metadataFile = $filepath . '.meta';
if (file_exists($filepath)) {
unlink($filepath);
}
if (file_exists($metadataFile)) {
unlink($metadataFile);
}
}
/**
* Set current image
*/
public function setCurrentImage($filename, $mimeType = 'image/png') {
$_SESSION['current_image_path'] = $filename;
$_SESSION['current_image_mime'] = $mimeType;
}
/**
* Get current image
*/
public function getCurrentImage() {
if (!isset($_SESSION['current_image_path']) || !$_SESSION['current_image_path']) {
return null;
}
return $this->getImage($_SESSION['current_image_path']);
}
/**
* Clear current image
*/
public function clearCurrentImage() {
if (isset($_SESSION['current_image_path']) && $_SESSION['current_image_path']) {
$this->deleteImage($_SESSION['current_image_path']);
}
$_SESSION['current_image_path'] = null;
$_SESSION['current_image_mime'] = 'image/png';
}
/**
* Add to conversation history
*/
public function addToHistory($prompt, $type = 'edit') {
$_SESSION['conversation_history'][] = [
'prompt' => $prompt,
'type' => $type,
'timestamp' => time()
];
// Keep only last 50 items
if (count($_SESSION['conversation_history']) > 50) {
$_SESSION['conversation_history'] = array_slice($_SESSION['conversation_history'], -50);
}
}
/**
* Get conversation history
*/
public function getHistory() {
return $_SESSION['conversation_history'] ?? [];
}
/**
* Clear all session data and images
*/
public function reset() {
// Delete all images in session directory
$imagesDir = $this->getImagesDir();
if (is_dir($imagesDir)) {
$files = glob($imagesDir . '/*');
foreach ($files as $file) {
if (is_file($file)) {
unlink($file);
}
}
}
// Clear session data
$_SESSION['conversation_history'] = [];
$_SESSION['image_history'] = [];
$_SESSION['current_image_path'] = null;
$_SESSION['current_image_mime'] = 'image/png';
}
/**
* Get file extension from MIME type
*/
private function getExtensionFromMime($mimeType) {
$mimeMap = [
'image/jpeg' => 'jpg',
'image/jpg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
'image/gif' => 'gif'
];
return $mimeMap[$mimeType] ?? 'png';
}
/**
* Get MIME type from file
*/
private function getMimeTypeFromFile($filepath) {
$extension = strtolower(pathinfo($filepath, PATHINFO_EXTENSION));
$extMap = [
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/png',
'webp' => 'image/webp',
'gif' => 'image/gif'
];
return $extMap[$extension] ?? 'image/png';
}
/**
* Static method to clean up expired images across all sessions
*/
public static function cleanupExpiredImages($uploadDir = null) {
if ($uploadDir === null) {
$uploadDir = __DIR__ . '/uploads/sessions';
}
if (!is_dir($uploadDir)) {
return ['cleaned' => 0, 'errors' => []];
}
$cleaned = 0;
$errors = [];
$currentTime = time();
// Scan all session directories
$sessionDirs = glob($uploadDir . '/*', GLOB_ONLYDIR);
foreach ($sessionDirs as $sessionDir) {
$imagesDir = $sessionDir . '/images';
if (!is_dir($imagesDir)) {
continue;
}
// Find all metadata files
$metadataFiles = glob($imagesDir . '/*.meta');
foreach ($metadataFiles as $metadataFile) {
try {
$metadata = json_decode(file_get_contents($metadataFile), true);
// Check if expired
if ($metadata && isset($metadata['expires_at']) && $metadata['expires_at'] < $currentTime) {
// Delete image and metadata
$imageFile = str_replace('.meta', '', $metadataFile);
if (file_exists($imageFile)) {
unlink($imageFile);
}
unlink($metadataFile);
$cleaned++;
}
} catch (Exception $e) {
$errors[] = $e->getMessage();
}
}
// Remove empty session directories
$remainingFiles = glob($imagesDir . '/*');
if (empty($remainingFiles)) {
rmdir($imagesDir);
rmdir($sessionDir);
}
}
return [
'cleaned' => $cleaned,
'errors' => $errors,
'timestamp' => date('Y-m-d H:i:s')
];
}
}