Implement file upload handling with size and MIME type validation, and add configuration for max upload size and storage path
Some checks failed
CI / release (push) Has been cancelled
CI / debug (push) Has been cancelled
CI / check-phpunit (push) Has been cancelled
CI / check-phpdoc (push) Has been cancelled
CI / generate-phpdoc (push) Has been cancelled
CI / test (push) Has been cancelled
CI / release-documentation (push) Has been cancelled
CI / release-artifacts (push) Has been cancelled
Some checks failed
CI / release (push) Has been cancelled
CI / debug (push) Has been cancelled
CI / check-phpunit (push) Has been cancelled
CI / check-phpdoc (push) Has been cancelled
CI / generate-phpdoc (push) Has been cancelled
CI / test (push) Has been cancelled
CI / release-documentation (push) Has been cancelled
CI / release-artifacts (push) Has been cancelled
This commit is contained in:
parent
14ed24049e
commit
f341af7ea5
6 changed files with 230 additions and 68 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace FederationServer\Classes\Configuration;
|
||||
|
||||
class FileStorageConfiguration
|
||||
{
|
||||
private string $path;
|
||||
private int $maxSize;
|
||||
|
||||
/**
|
||||
* FileStorageConfiguration constructor.
|
||||
*
|
||||
* @param array $config Array with file storage configuration values.
|
||||
*/
|
||||
public function __construct(array $config)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
|
177
src/FederationServer/Classes/FileUploadHandler.php
Normal file
177
src/FederationServer/Classes/FileUploadHandler.php
Normal file
|
@ -0,0 +1,177 @@
|
|||
<?php
|
||||
|
||||
namespace FederationServer\Classes;
|
||||
|
||||
use Exception;
|
||||
use FederationServer\Classes\Managers\FileAttachmentManager;
|
||||
use FederationServer\Exceptions\FileUploadException;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class FileUploadHandler
|
||||
{
|
||||
/**
|
||||
* Handle a file upload request.
|
||||
*
|
||||
* @param array $file The $_FILES array element for the uploaded file
|
||||
* @param string $evidence The UUID of the evidence this file is attached to
|
||||
* @return void Information about the uploaded file, including UUID and filename
|
||||
* @throws FileUploadException If there's an issue with the upload
|
||||
*/
|
||||
public static function handleUpload(array $file, string $evidence): void
|
||||
{
|
||||
if (!isset($file) || !is_array($file) || !isset($file['tmp_name']) || empty($file['tmp_name']))
|
||||
{
|
||||
throw new InvalidArgumentException('Invalid file upload data provided');
|
||||
}
|
||||
|
||||
if (empty($evidence))
|
||||
{
|
||||
throw new InvalidArgumentException('Evidence ID is required');
|
||||
}
|
||||
|
||||
// Validate the file size
|
||||
if (!isset($file['size']) || $file['size'] <= 0)
|
||||
{
|
||||
throw new FileUploadException('Invalid file size');
|
||||
}
|
||||
|
||||
if ($file['size'] > 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
21
src/FederationServer/Exceptions/FileUploadException.php
Normal file
21
src/FederationServer/Exceptions/FileUploadException.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace FederationServer\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
class FileUploadException extends Exception
|
||||
{
|
||||
/**
|
||||
* FileUploadException constructor.
|
||||
*
|
||||
* @param string $message The error message
|
||||
* @param int $code The error code (default is 0)
|
||||
* @param Throwable|null $previous Previous exception for chaining (default is null)
|
||||
*/
|
||||
public function __construct(string $message, int $code = 0, ?Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue