diff --git a/src/FederationServer/Classes/Configuration.php b/src/FederationServer/Classes/Configuration.php index 1130886..4e49423 100644 --- a/src/FederationServer/Classes/Configuration.php +++ b/src/FederationServer/Classes/Configuration.php @@ -13,7 +13,6 @@ private static ?\ConfigLib\Configuration $configuration = null; private static ?DatabaseConfiguration $databaseConfiguration = null; private static ?RedisConfiguration $redisConfiguration = null; - private static ?FileStorageConfiguration $fileStorageConfiguration = null; /** * Initialize the configuration with default values. @@ -24,6 +23,8 @@ self::$configuration->setDefault('server.name', 'Federation Server'); self::$configuration->setDefault('server.api_key', Utilities::generateString()); + self::$configuration->setDefault('server.max_upload_size', 52428800); // 50MB default + self::$configuration->setDefault('server.storage_path', '/var/www/uploads'); self::$configuration->setDefault('database.host', '127.0.0.1'); self::$configuration->setDefault('database.port', 3306); @@ -38,16 +39,11 @@ self::$configuration->setDefault('redis.port', 6379); self::$configuration->setDefault('redis.password', null); self::$configuration->setDefault('redis.database', 0); - - self::$configuration->setDefault('file_storage.max_size', 52428800); // 50 MB - self::$configuration->setDefault('file_storage.path', '/var/www/uploads'); - self::$configuration->save(); self::$serverConfiguration = new ServerConfiguration(self::$configuration->get('server')); self::$databaseConfiguration = new DatabaseConfiguration(self::$configuration->get('database')); self::$redisConfiguration = new RedisConfiguration(self::$configuration->get('redis')); - self::$fileStorageConfiguration = new FileStorageConfiguration(self::$configuration->get('file_storage')); } /** @@ -124,20 +120,5 @@ return self::$redisConfiguration; } - - /** - * Get the file storage configuration. - * - * @return FileStorageConfiguration - */ - public static function getFileStorageConfiguration(): FileStorageConfiguration - { - if(self::$fileStorageConfiguration === null) - { - self::initialize(); - } - - return self::$fileStorageConfiguration; - } } diff --git a/src/FederationServer/Classes/Configuration/FileStorageConfiguration.php b/src/FederationServer/Classes/Configuration/FileStorageConfiguration.php deleted file mode 100644 index 5cf9aee..0000000 --- a/src/FederationServer/Classes/Configuration/FileStorageConfiguration.php +++ /dev/null @@ -1,41 +0,0 @@ -path = $config['path'] ?? '/var/www/uploads'; - $this->maxSize = $config['max_size'] ?? 52428800; - } - - /** - * Get the file storage path. - * - * @return string - */ - public function getPath(): string - { - return $this->path; - } - - /** - * Get the maximum file size allowed for uploads. - * - * @return int - */ - public function getMaxSize(): int - { - return $this->maxSize; - } - } - diff --git a/src/FederationServer/Classes/Configuration/ServerConfiguration.php b/src/FederationServer/Classes/Configuration/ServerConfiguration.php index 81a1765..e8ed77e 100644 --- a/src/FederationServer/Classes/Configuration/ServerConfiguration.php +++ b/src/FederationServer/Classes/Configuration/ServerConfiguration.php @@ -6,6 +6,8 @@ { private string $name; private ?string $apiKey; + private int $maxUploadSize; + private string $storagePath; /** * ServerConfiguration constructor. @@ -16,6 +18,8 @@ { $this->name = $config['server.name'] ?? 'Federation Server'; $this->apiKey = $config['server.api_key'] ?? null; + $this->maxUploadSize = $config['max_upload_size'] ?? 52428800; // 50MB default + $this->storagePath = $config['server.storage_path'] ?? '/var/www/uploads'; } /** @@ -37,4 +41,24 @@ { return $this->apiKey; } - } \ No newline at end of file + + /** + * Get the maximum allowed upload size in bytes. + * + * @return int Maximum upload size in bytes + */ + public function getMaxUploadSize(): int + { + return $this->maxUploadSize; + } + + /** + * Get the path where files are stored. + * + * @return string The storage path for uploaded files. + */ + public function getStoragePath(): string + { + return $this->storagePath; + } + } diff --git a/src/FederationServer/Classes/FileUploadHandler.php b/src/FederationServer/Classes/FileUploadHandler.php new file mode 100644 index 0000000..35c5182 --- /dev/null +++ b/src/FederationServer/Classes/FileUploadHandler.php @@ -0,0 +1,177 @@ + Configuration::getServerConfiguration()->getMaxUploadSize()) + { + throw new FileUploadException('File exceeds maximum allowed size'); + } + + // Validate file upload status + if (!isset($file['error']) || $file['error'] !== UPLOAD_ERR_OK) + { + $errorMessage = self::getUploadErrorMessage($file['error'] ?? -1); + throw new FileUploadException($errorMessage); + } + + $detectedMimeType = self::detectMimeType($file['tmp_name']); + $uuid = Uuid::v4()->toRfc4122(); + $originalName = self::getSafeFileName($file['name'] ?? 'unnamed'); + + // Get file storage path and ensure the directory exists + $storagePath = rtrim(Configuration::getServerConfiguration()->getStoragePath(), DIRECTORY_SEPARATOR); + if (!is_dir($storagePath)) + { + if (!mkdir($storagePath, 0750, true)) + { + throw new FileUploadException('Storage directory could not be created'); + } + } + + // Prepare destination path (UUID only, no extension as per requirements) + $destinationPath = $storagePath . DIRECTORY_SEPARATOR . $uuid; + if (!move_uploaded_file($file['tmp_name'], $destinationPath)) + { + throw new FileUploadException('Failed to save uploaded file'); + } + + chmod($destinationPath, 0640); + + try + { + // Create a record in the database + FileAttachmentManager::createRecord($uuid, $evidence, $detectedMimeType, $originalName, $file['size']); + } + catch (Exception $e) + { + // If database insertion fails, remove the file to maintain consistency + @unlink($destinationPath); + throw new FileUploadException('Failed to record file upload: ' . $e->getMessage()); + } + finally + { + // Clean up temporary files if needed + if (file_exists($file['tmp_name'])) + { + @unlink($file['tmp_name']); + } + } + } + + /** + * Get human-readable error message for PHP upload error codes + * + * @param int $errorCode PHP upload error code + * @return string Human-readable error message + */ + private static function getUploadErrorMessage(int $errorCode): string + { + return match ($errorCode) + { + UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => 'The uploaded file exceeds the maximum allowed size', + UPLOAD_ERR_PARTIAL => 'The file was only partially uploaded', + UPLOAD_ERR_NO_FILE => 'No file was uploaded', + UPLOAD_ERR_NO_TMP_DIR => 'Missing temporary folder', + UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk', + UPLOAD_ERR_EXTENSION => 'File upload stopped by extension', + default => 'Unknown upload error', + }; + } + + /** + * Safely detect the MIME type of a file + * + * @param string $filePath Path to the file + * @return string The detected MIME type + */ + private static function detectMimeType(string $filePath): string + { + // Using multiple methods for better reliability + + // First try with Fileinfo extension (most reliable) + if (function_exists('finfo_open')) + { + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mimeType = finfo_file($finfo, $filePath); + finfo_close($finfo); + if ($mimeType) + { + return $mimeType; + } + } + + // Then try with mime_content_type + if (function_exists('mime_content_type')) + { + $mimeType = mime_content_type($filePath); + if ($mimeType) + { + return $mimeType; + } + } + + // Fall back to a simple extension-based check as last resort + return 'application/octet-stream'; + } + + /** + * Get a safe filename by removing potentially unsafe characters + * + * @param string $filename Original filename + * @return string Sanitized filename + */ + private static function getSafeFileName(string $filename): string + { + // Remove any path information to avoid directory traversal + $filename = basename($filename); + + // Remove null bytes and other control characters + $filename = preg_replace('/[\x00-\x1F\x7F]/u', '', $filename); + + // Limit length to avoid extremely long filenames + if (strlen($filename) > 255) + { + $extension = pathinfo($filename, PATHINFO_EXTENSION); + $baseFilename = pathinfo($filename, PATHINFO_FILENAME); + $filename = substr($baseFilename, 0, 245) . '.' . $extension; + } + + return $filename; + } + } + + diff --git a/src/FederationServer/Classes/Managers/FileAttachmentManager.php b/src/FederationServer/Classes/Managers/FileAttachmentManager.php index b8230eb..1213024 100644 --- a/src/FederationServer/Classes/Managers/FileAttachmentManager.php +++ b/src/FederationServer/Classes/Managers/FileAttachmentManager.php @@ -17,11 +17,10 @@ * @param string $uuid The UUID of the file attachment. * @param string $evidence The UUID of the evidence associated with the file attachment. * @param string $fileName The name of the file being attached. - * @param string $fileSize The size of the file in bytes. + * @param int $fileSize The size of the file in bytes. * @throws DatabaseOperationException If there is an error preparing or executing the SQL statement. - * @throws InvalidArgumentException If the file name exceeds 255 characters or if the file size is not a positive integer. */ - public static function createRecord(string $uuid, string $evidence, string $fileName, string $fileSize): void + public static function createRecord(string $uuid, string $evidence, string $fileMime, string $fileName, int $fileSize): void { if(strlen($fileName) > 255) { @@ -35,11 +34,12 @@ try { - $stmt = DatabaseConnection::getConnection()->prepare("INSERT INTO file_attachments (uuid, evidence, file_name, file_size) VALUES (:uuid, :evidence, :file_name, :file_size)"); + $stmt = DatabaseConnection::getConnection()->prepare("INSERT INTO file_attachments (uuid, evidence, file_mime, file_name, file_size) VALUES (:uuid, :evidence, :file_mime, :file_name, :file_size)"); $stmt->bindParam(':uuid', $uuid); $stmt->bindParam(':evidence', $evidence); + $stmt->bindParam(':file_mime', $fileMime); $stmt->bindParam(':file_name', $fileName); - $stmt->bindParam(':file_size', $fileSize); + $stmt->bindParam(':file_size', $fileSize, PDO::PARAM_INT); $stmt->execute(); } diff --git a/src/FederationServer/Exceptions/FileUploadException.php b/src/FederationServer/Exceptions/FileUploadException.php new file mode 100644 index 0000000..bc6a325 --- /dev/null +++ b/src/FederationServer/Exceptions/FileUploadException.php @@ -0,0 +1,21 @@ +