diff --git a/.gitignore b/.gitignore index 8eeeeb3..8cb01e4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,10 @@ config.php # PHP session files sessions/ +# User uploaded images (24-hour storage) +uploads/sessions/ +!uploads/.htaccess + # IDE files .vscode/ .idea/ diff --git a/api.php b/api.php index 2625691..7912150 100644 --- a/api.php +++ b/api.php @@ -1,9 +1,12 @@ reset(); echo json_encode(['success' => true]); exit; @@ -207,13 +207,10 @@ try { } // Handle uploaded image - if ($uploadedImage && !$_SESSION['current_image']) { + $currentImage = $sessionManager->getCurrentImage(); + if ($uploadedImage && !$currentImage) { error_log("Processing uploaded image (type: $uploadedImageType)"); - // Store the uploaded image directly in session - $_SESSION['current_image'] = $uploadedImage; - $_SESSION['current_image_mime'] = $uploadedImageType; - // If there's a prompt, apply it to the uploaded image if ($prompt) { error_log("Applying prompt to uploaded image: $prompt"); @@ -225,23 +222,19 @@ try { $response = $api->generateImage($prompt, $aspectRatio, $imageSize, $uploadedImage); $imageData = $api->extractImageData($response); - // Update session with the edited image - $_SESSION['current_image'] = $imageData['base64']; - $_SESSION['current_image_mime'] = $imageData['mime_type']; + // Save edited image to disk + $filename = $sessionManager->saveImage($imageData['base64'], $imageData['mime_type']); + $sessionManager->setCurrentImage($filename, $imageData['mime_type']); // Add to conversation history - $_SESSION['conversation_history'][] = [ - 'prompt' => 'Uploaded image + ' . $prompt, - 'timestamp' => time(), - 'type' => 'upload_edit' - ]; + $sessionManager->addToHistory('Uploaded image + ' . $prompt, 'upload_edit'); } else { - // Just uploaded, no prompt yet - $_SESSION['conversation_history'][] = [ - 'prompt' => 'Image uploaded', - 'timestamp' => time(), - 'type' => 'upload' - ]; + // Just uploaded, no prompt yet - save to disk + $filename = $sessionManager->saveImage($uploadedImage, $uploadedImageType); + $sessionManager->setCurrentImage($filename, $uploadedImageType); + + // Add to conversation history + $sessionManager->addToHistory('Image uploaded', 'upload'); } echo json_encode([ @@ -260,29 +253,19 @@ try { $api = new NanoBananaProAPI(GEMINI_API_KEY); // Get current image if editing - $inputImage = $_SESSION['current_image'] ?? null; + $currentImage = $sessionManager->getCurrentImage(); + $inputImage = $currentImage ? $currentImage['data'] : null; // Generate or edit image $response = $api->generateImage($prompt, $aspectRatio, $imageSize, $inputImage); $imageData = $api->extractImageData($response); - // Save to session - $_SESSION['current_image'] = $imageData['base64']; - $_SESSION['current_image_mime'] = $imageData['mime_type']; + // Save to disk + $filename = $sessionManager->saveImage($imageData['base64'], $imageData['mime_type']); + $sessionManager->setCurrentImage($filename, $imageData['mime_type']); // Add to conversation history - $_SESSION['conversation_history'][] = [ - 'prompt' => $prompt, - 'timestamp' => time(), - 'type' => $inputImage ? 'edit' : 'generate' - ]; - - // Add to image history - $_SESSION['image_history'][] = [ - 'image' => $imageData['base64'], - 'prompt' => $prompt, - 'timestamp' => time() - ]; + $sessionManager->addToHistory($prompt, $inputImage ? 'edit' : 'generate'); echo json_encode([ 'success' => true, diff --git a/cleanup.php b/cleanup.php new file mode 100644 index 0000000..2e35054 --- /dev/null +++ b/cleanup.php @@ -0,0 +1,64 @@ +> /path/to/cleanup.log 2>&1 + */ + +require_once 'session_manager.php'; + +// Check if running from command line or web +$isCLI = php_sapi_name() === 'cli'; + +if (!$isCLI) { + // If accessed via web, require authentication or disable + // For now, we'll allow it but you should add authentication in production + header('Content-Type: application/json'); +} + +try { + $result = SessionManager::cleanupExpiredImages(); + + $output = [ + 'success' => true, + 'message' => "Cleanup completed successfully", + 'cleaned_images' => $result['cleaned'], + 'errors' => $result['errors'], + 'timestamp' => $result['timestamp'] + ]; + + if ($isCLI) { + echo "=== Image Cleanup Report ===\n"; + echo "Timestamp: {$result['timestamp']}\n"; + echo "Images cleaned: {$result['cleaned']}\n"; + if (!empty($result['errors'])) { + echo "Errors encountered: " . count($result['errors']) . "\n"; + foreach ($result['errors'] as $error) { + echo " - $error\n"; + } + } + echo "===========================\n\n"; + } else { + echo json_encode($output, JSON_PRETTY_PRINT); + } + + exit(0); + +} catch (Exception $e) { + $output = [ + 'success' => false, + 'error' => $e->getMessage(), + 'timestamp' => date('Y-m-d H:i:s') + ]; + + if ($isCLI) { + echo "ERROR: {$e->getMessage()}\n"; + exit(1); + } else { + http_response_code(500); + echo json_encode($output, JSON_PRETTY_PRINT); + exit(1); + } +} diff --git a/index.php b/index.php index f65ceb3..37f4390 100644 --- a/index.php +++ b/index.php @@ -1,19 +1,11 @@ getCurrentImage(); +$conversationHistory = $sessionManager->getHistory(); ?> @@ -774,7 +766,7 @@ if (!isset($_SESSION['image_history'])) {
Has Current Image: -Image MIME Type: -Image Data Length: -Conversation History: items -Image History: items+
Session ID: getSessionId(); ?> +Has Current Image: +Image MIME Type: +Image Data Length: +Conversation History: items +Session Directory: getSessionDir()); ?>
items// Check if we have either a prompt or an upload const hasUploadFile = uploadInput && uploadInput.files && uploadInput.files[0]; - const hasExistingImage = ; + const hasExistingImage = ; if (!prompt && !hasUploadFile && !hasExistingImage) { showError('Please enter a prompt or upload an image'); @@ -991,7 +984,7 @@ Image History: items prompt: prompt, aspectRatio: aspectRatio, imageSize: imageSize, - hasExistingImage: , + hasExistingImage: , hasUpload: hasUpload }); @@ -1014,17 +1007,17 @@ Image History: items showError(result.error || 'Failed to generate image'); logDebug('error', result.error || 'Failed to generate image'); generateBtn.disabled = false; - generateBtn.textContent = ''; + generateBtn.textContent = ''; } } catch (error) { showError('Network error: ' + error.message); logDebug('error', 'Network error: ' + error.message); generateBtn.disabled = false; - generateBtn.textContent = ''; + generateBtn.textContent = ''; } }); - + resetBtn.addEventListener('click', async () => { if (confirm('Are you sure you want to start a new image? This will clear your current image and history.')) { const formData = new FormData(); diff --git a/session_manager.php b/session_manager.php new file mode 100644 index 0000000..b148fa6 --- /dev/null +++ b/session_manager.php @@ -0,0 +1,314 @@ +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') + ]; + } +} diff --git a/uploads/.htaccess b/uploads/.htaccess new file mode 100644 index 0000000..312aaf3 --- /dev/null +++ b/uploads/.htaccess @@ -0,0 +1,11 @@ +# Secure uploads directory +# Prevent direct access to uploaded images via browser + +# Deny access to all files +Order Deny,Allow +Deny from all + +# Allow PHP scripts to access files +