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(); // Clean up invalid history entries $this->cleanupImageHistory(); // Auto-cleanup expired images (runs randomly ~10% of the time) // This replaces the need for a cron job if (rand(1, 10) === 1) { $this->autoCleanupExpiredImages(); } } 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; // Check if file exists and is actually a file (not a directory) if (!file_exists($filepath) || !is_file($filepath)) { return null; } // Check if image has expired $metadataFile = $filepath . '.meta'; if (file_exists($metadataFile) && is_file($metadataFile)) { $metadata = json_decode(file_get_contents($metadataFile), true); if ($metadata && isset($metadata['expires_at']) && $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 and add to history */ public function setCurrentImage($filename, $mimeType = 'image/png') { $_SESSION['current_image_path'] = $filename; $_SESSION['current_image_mime'] = $mimeType; // Add to history (keep last 10) $this->addToImageHistory($filename, $mimeType); } /** * Add image to history */ private function addToImageHistory($filename, $mimeType) { if (!isset($_SESSION['image_history'])) { $_SESSION['image_history'] = []; } // Add to beginning of array array_unshift($_SESSION['image_history'], [ 'filename' => $filename, 'mime_type' => $mimeType, 'timestamp' => time() ]); // Keep only last 10 if (count($_SESSION['image_history']) > 10) { $_SESSION['image_history'] = array_slice($_SESSION['image_history'], 0, 10); } } /** * Get image history */ public function getImageHistory() { return $_SESSION['image_history'] ?? []; } /** * Clean up invalid image history entries * Removes entries without filename or with missing files */ private function cleanupImageHistory() { if (!isset($_SESSION['image_history']) || !is_array($_SESSION['image_history'])) { $_SESSION['image_history'] = []; return; } $validHistory = []; foreach ($_SESSION['image_history'] as $item) { // Check if item has filename key and file exists if (isset($item['filename']) && !empty($item['filename'])) { $filepath = $this->getImagesDir() . '/' . $item['filename']; if (file_exists($filepath) && is_file($filepath)) { $validHistory[] = $item; } } } $_SESSION['image_history'] = $validHistory; } /** * Restore image from history */ public function restoreImageFromHistory($filename) { $image = $this->getImage($filename); if ($image) { $this->setCurrentImage($filename, $image['mime_type']); return true; } return false; } /** * 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'; } /** * Auto-cleanup expired images (called on session init) * Runs the cleanup in the background without blocking */ private function autoCleanupExpiredImages() { try { // Run cleanup silently $result = self::cleanupExpiredImages($this->uploadDir); // Log cleanup results if any images were cleaned if ($result['cleaned'] > 0) { error_log("Auto-cleanup: Removed {$result['cleaned']} expired images"); } } catch (Exception $e) { // Silently fail - don't break the app if cleanup fails error_log("Auto-cleanup failed: " . $e->getMessage()); } } /** * 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') ]; } }