uploadDir = rtrim($config['upload_dir'] ?? 'albums', '/'); $this->maxFileSize = $config['upload_max_file_size'] ?? 10485760; // 10MB default $this->allowedTypes = $config['upload_allowed_types'] ?? [ 'image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/tiff', 'image/webp' ]; $this->imageProcessor = $imageProcessor; $this->dataManager = $dataManager; } /** * Handle file upload for multiple files * @param int $albumId Target album ID * @param array $files $_FILES array or single file array * @return array Upload results with success/error information */ public function handleUpload($albumId, $files) { $results = []; try { // Validate album exists $album = $this->dataManager->getAlbum($albumId); if (!$album) { throw new Exception("Album with ID {$albumId} not found"); } // Ensure album directory exists $albumDir = $this->uploadDir . '/' . $albumId; if (!is_dir($albumDir)) { if (!mkdir($albumDir, 0755, true)) { throw new Exception("Failed to create album directory: {$albumDir}"); } } // Handle single file or multiple files if (isset($files['tmp_name']) && !is_array($files['tmp_name'])) { // Single file upload $results[] = $this->processUploadedFile($files, $albumId); } else { // Multiple file upload $fileCount = count($files['tmp_name']); for ($i = 0; $i < $fileCount; $i++) { $singleFile = [ 'name' => $files['name'][$i], 'type' => $files['type'][$i], 'tmp_name' => $files['tmp_name'][$i], 'error' => $files['error'][$i], 'size' => $files['size'][$i] ]; $results[] = $this->processUploadedFile($singleFile, $albumId); } } return [ 'success' => true, 'results' => $results, 'total_files' => count($results), 'successful_uploads' => count(array_filter($results, function($r) { return $r['success']; })) ]; } catch (Exception $e) { return [ 'success' => false, 'error' => $e->getMessage(), 'results' => $results ]; } } /** * Process a single uploaded file * @param array $file Single file from $_FILES * @param int $albumId Target album ID * @return array Processing result */ public function processUploadedFile($file, $albumId) { try { // Validate file upload $validation = $this->validateFile($file); if (!$validation['valid']) { return [ 'success' => false, 'filename' => $file['name'], 'error' => $validation['error'] ]; } // Generate unique filename $uniqueFilename = $this->generateUniqueFilename($file['name'], $albumId); $tempPath = $file['tmp_name']; $finalPath = $this->uploadDir . '/' . $albumId . '/' . $uniqueFilename; // Validate image and get metadata $imageInfo = $this->imageProcessor->validateImage($tempPath); // Convert to WebP and resize $conversionResult = $this->imageProcessor->convertToWebP($tempPath, $finalPath); // Generate thumbnail $thumbnailPath = $this->uploadDir . '/' . $albumId . '/thumb_' . $uniqueFilename; $thumbnailResult = $this->imageProcessor->generateThumbnail($finalPath, $thumbnailPath); // Add photo to database $photoData = [ 'filename' => $uniqueFilename, 'original_filename' => $file['name'], 'file_size' => $conversionResult['file_size'], 'width' => $conversionResult['new_width'], 'height' => $conversionResult['new_height'] ]; $photoId = $this->dataManager->addPhoto($albumId, $photoData); return [ 'success' => true, 'photo_id' => $photoId, 'filename' => $uniqueFilename, 'original_filename' => $file['name'], 'file_size' => $conversionResult['file_size'], 'dimensions' => [ 'original' => [ 'width' => $imageInfo['width'], 'height' => $imageInfo['height'] ], 'processed' => [ 'width' => $conversionResult['new_width'], 'height' => $conversionResult['new_height'] ] ], 'thumbnail_generated' => true, 'conversion_applied' => true ]; } catch (Exception $e) { return [ 'success' => false, 'filename' => $file['name'] ?? 'unknown', 'error' => $e->getMessage() ]; } } /** * Validate uploaded file * @param array $file File array from $_FILES * @return array Validation result with valid flag and error message */ public function validateFile($file) { // Check for upload errors if ($file['error'] !== UPLOAD_ERR_OK) { return [ 'valid' => false, 'error' => $this->getUploadErrorMessage($file['error']) ]; } // Check if file was actually uploaded if (!is_uploaded_file($file['tmp_name'])) { return [ 'valid' => false, 'error' => 'File was not uploaded via HTTP POST' ]; } // Check file size if ($file['size'] > $this->maxFileSize) { return [ 'valid' => false, 'error' => 'File size exceeds maximum allowed size of ' . $this->formatBytes($this->maxFileSize) ]; } // Check file size is not zero if ($file['size'] === 0) { return [ 'valid' => false, 'error' => 'File is empty' ]; } // Validate MIME type $mimeType = mime_content_type($file['tmp_name']); if (!$mimeType || !in_array($mimeType, $this->allowedTypes)) { return [ 'valid' => false, 'error' => 'File type not allowed. Allowed types: ' . implode(', ', $this->allowedTypes) ]; } // Additional security check - verify file extension matches MIME type $extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); $expectedExtensions = $this->getMimeTypeExtensions($mimeType); if (!in_array($extension, $expectedExtensions)) { return [ 'valid' => false, 'error' => 'File extension does not match file type' ]; } // Validate that it's actually an image using getimagesize $imageInfo = getimagesize($file['tmp_name']); if ($imageInfo === false) { return [ 'valid' => false, 'error' => 'File is not a valid image' ]; } return [ 'valid' => true, 'mime_type' => $mimeType, 'extension' => $extension, 'image_info' => $imageInfo ]; } /** * Generate unique filename to prevent conflicts * @param string $originalName Original filename * @param int $albumId Album ID * @return string Unique filename with .webp extension */ private function generateUniqueFilename($originalName, $albumId) { // Get base name without extension $baseName = pathinfo($originalName, PATHINFO_FILENAME); // Sanitize filename - remove special characters and spaces $baseName = preg_replace('/[^a-zA-Z0-9_-]/', '_', $baseName); $baseName = trim($baseName, '_'); // Ensure filename is not empty if (empty($baseName)) { $baseName = 'image'; } // Limit length $baseName = substr($baseName, 0, 50); $albumDir = $this->uploadDir . '/' . $albumId; $extension = '.webp'; // All images are converted to WebP $counter = 0; // Ensure album directory exists for file existence check if (!is_dir($albumDir)) { mkdir($albumDir, 0755, true); } do { if ($counter === 0) { $filename = $baseName . $extension; } else { $filename = $baseName . '_' . $counter . $extension; } $counter++; } while (file_exists($albumDir . '/' . $filename)); return $filename; } /** * Get expected file extensions for a MIME type * @param string $mimeType MIME type * @return array Array of expected extensions */ private function getMimeTypeExtensions($mimeType) { $mimeToExtensions = [ 'image/jpeg' => ['jpg', 'jpeg'], 'image/png' => ['png'], 'image/gif' => ['gif'], 'image/bmp' => ['bmp'], 'image/x-ms-bmp' => ['bmp'], 'image/tiff' => ['tiff', 'tif'], 'image/webp' => ['webp'] ]; return $mimeToExtensions[$mimeType] ?? []; } /** * Get human-readable upload error message * @param int $errorCode PHP upload error code * @return string Error message */ private function getUploadErrorMessage($errorCode) { switch ($errorCode) { case UPLOAD_ERR_INI_SIZE: return 'File exceeds the upload_max_filesize directive in php.ini'; case UPLOAD_ERR_FORM_SIZE: return 'File exceeds the MAX_FILE_SIZE directive in the HTML form'; case UPLOAD_ERR_PARTIAL: return 'File was only partially uploaded'; case UPLOAD_ERR_NO_FILE: return 'No file was uploaded'; case UPLOAD_ERR_NO_TMP_DIR: return 'Missing temporary folder'; case UPLOAD_ERR_CANT_WRITE: return 'Failed to write file to disk'; case UPLOAD_ERR_EXTENSION: return 'File upload stopped by extension'; default: return 'Unknown upload error'; } } /** * Format bytes to human readable format * @param int $bytes Number of bytes * @return string Formatted string */ private function formatBytes($bytes) { $units = ['B', 'KB', 'MB', 'GB']; $bytes = max($bytes, 0); $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); $pow = min($pow, count($units) - 1); $bytes /= pow(1024, $pow); return round($bytes, 2) . ' ' . $units[$pow]; } /** * Get upload directory path * @return string Upload directory path */ public function getUploadDir() { return $this->uploadDir; } /** * Get maximum file size * @return int Maximum file size in bytes */ public function getMaxFileSize() { return $this->maxFileSize; } /** * Get allowed file types * @return array Array of allowed MIME types */ public function getAllowedTypes() { return $this->allowedTypes; } /** * Check if storage limit would be exceeded * @param int $additionalSize Size of new files to be added * @return array Result with can_upload flag and current usage info */ public function checkStorageLimit($additionalSize = 0) { try { $maxTotalSize = $this->dataManager->getConfig('storage_max_total_size'); if (!$maxTotalSize) { return [ 'can_upload' => true, 'unlimited' => true ]; } $currentUsage = $this->calculateStorageUsage(); $projectedUsage = $currentUsage + $additionalSize; return [ 'can_upload' => $projectedUsage <= $maxTotalSize, 'current_usage' => $currentUsage, 'max_size' => $maxTotalSize, 'projected_usage' => $projectedUsage, 'available_space' => max(0, $maxTotalSize - $currentUsage), 'usage_percentage' => round(($currentUsage / $maxTotalSize) * 100, 2) ]; } catch (Exception $e) { return [ 'can_upload' => true, 'error' => $e->getMessage() ]; } } /** * Calculate current storage usage * @return int Total storage usage in bytes */ private function calculateStorageUsage() { $totalSize = 0; if (!is_dir($this->uploadDir)) { return 0; } $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($this->uploadDir, RecursiveDirectoryIterator::SKIP_DOTS) ); foreach ($iterator as $file) { if ($file->isFile()) { $totalSize += $file->getSize(); } } return $totalSize; } } ?>