Files
michaelschiemer/src/Framework/Filesystem/FileValidator.php
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

363 lines
10 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\Filesystem;
use App\Framework\Core\ValueObjects\FileSize;
use App\Framework\Exception\Core\FilesystemErrorCode;
use App\Framework\Filesystem\Exceptions\FileValidationException;
use App\Framework\Filesystem\Exceptions\FileNotFoundException;
use App\Framework\Filesystem\Exceptions\FilePermissionException;
use App\Framework\Filesystem\Exceptions\DirectoryCreateException;
use App\Framework\Filesystem\ValueObjects\FilePath;
/**
* Zentrale Validierung für Filesystem-Operationen
*
* Validiert Pfade, Permissions, File Sizes und andere Constraints
* bevor Filesystem-Operationen ausgeführt werden
*/
final readonly class FileValidator
{
private ?array $allowedExtensionsMap;
private ?array $blockedExtensionsMap;
private string $pathTraversalPattern;
public function __construct(
private ?array $allowedExtensions = null,
private ?array $blockedExtensions = null,
private ?FileSize $maxFileSize = null,
private ?string $baseDirectory = null
) {
// Optimize extension lookups: O(n) → O(1)
$this->allowedExtensionsMap = $allowedExtensions !== null
? array_flip($allowedExtensions)
: null;
$this->blockedExtensionsMap = $blockedExtensions !== null
? array_flip($blockedExtensions)
: null;
// Compile path traversal patterns: 6 str_contains() → 1 regex
$this->pathTraversalPattern = '#(?:\.\./)' .
'|(?:\.\.\\\\)' .
'|(?:%2e%2e/)' .
'|(?:%2e%2e\\\\)' .
'|(?:\.\.%2f)' .
'|(?:\.\.%5c)#i';
}
/**
* Factory für Standard-Validator mit sicheren Defaults
*/
public static function createDefault(): self
{
return new self(
allowedExtensions: null, // Keine Einschränkung
blockedExtensions: ['exe', 'bat', 'sh', 'cmd', 'com'], // Executable files blockieren
maxFileSize: FileSize::fromMegabytes(100), // 100MB max
baseDirectory: null
);
}
/**
* Factory für strict Validator (nur whitelisted extensions)
*/
public static function createStrict(array $allowedExtensions): self
{
return new self(
allowedExtensions: $allowedExtensions,
blockedExtensions: null,
maxFileSize: FileSize::fromMegabytes(50),
baseDirectory: null
);
}
/**
* Factory für Upload-Validator
*/
public static function forUploads(?FileSize $maxSize = null): self
{
return new self(
allowedExtensions: ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'txt', 'csv', 'json'],
blockedExtensions: ['exe', 'bat', 'sh', 'cmd', 'com', 'php', 'phtml'],
maxFileSize: $maxSize ?? FileSize::fromMegabytes(10),
baseDirectory: null
);
}
/**
* Factory für Image-Validator
*/
public static function forImages(?FileSize $maxSize = null): self
{
return new self(
allowedExtensions: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'],
blockedExtensions: null,
maxFileSize: $maxSize ?? FileSize::fromMegabytes(5),
baseDirectory: null
);
}
/**
* Validiert Dateipfad
*
* @throws FileValidationException
*/
public function validatePath(string $path): void
{
// Empty path
if (empty($path)) {
throw FileValidationException::invalidPath(
$path,
'File path cannot be empty'
);
}
// Null bytes (security)
if (str_contains($path, "\0")) {
throw FileValidationException::invalidPath(
$path,
'File path contains null bytes'
);
}
// Path traversal attempt
if ($this->containsPathTraversal($path)) {
throw FileValidationException::invalidPath(
$path,
'Path traversal attempt detected',
FilesystemErrorCode::PATH_TRAVERSAL_ATTEMPT
);
}
// Base directory constraint
if ($this->baseDirectory !== null) {
$realPath = realpath($path);
$realBase = realpath($this->baseDirectory);
if ($realPath === false || $realBase === false) {
throw FileValidationException::invalidPath(
$path,
'Cannot resolve real path'
);
}
if (!str_starts_with($realPath, $realBase)) {
throw FileValidationException::invalidPath(
$realPath,
'Path is outside base directory',
FilesystemErrorCode::PATH_TRAVERSAL_ATTEMPT
);
}
}
}
/**
* Validiert Dateiendung
*
* @throws FileValidationException
*/
public function validateExtension(string $path): void
{
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
if (empty($extension)) {
// Keine Extension ist ok wenn keine Whitelist definiert
if ($this->allowedExtensions !== null) {
throw FileValidationException::invalidPath(
$path,
'File has no extension'
);
}
return;
}
// Whitelist check - O(1) lookup mit array_flip
if ($this->allowedExtensionsMap !== null) {
if (!isset($this->allowedExtensionsMap[$extension])) {
throw FileValidationException::invalidExtension(
$path,
$extension,
$this->allowedExtensions
);
}
}
// Blacklist check - O(1) lookup mit array_flip
if ($this->blockedExtensionsMap !== null) {
if (isset($this->blockedExtensionsMap[$extension])) {
throw FileValidationException::blockedExtension(
$path,
$extension,
$this->blockedExtensions
);
}
}
}
/**
* Validiert Dateigröße
*
* @throws FileValidationException
*/
public function validateFileSize(FileSize $fileSize): void
{
if ($this->maxFileSize === null) {
return;
}
if ($fileSize->exceedsLimit($this->maxFileSize)) {
throw FileValidationException::fileSizeExceeded(
$fileSize->toHumanReadable(),
$this->maxFileSize->toHumanReadable(),
$fileSize->toBytes(),
$this->maxFileSize->toBytes()
);
}
}
/**
* Validiert ob Datei existiert
*
* @throws FileNotFoundException
*/
public function validateExists(string $path): void
{
if (!file_exists($path)) {
throw new FileNotFoundException($path);
}
}
/**
* Validiert ob Pfad beschreibbar ist
*
* @throws FilePermissionException
*/
public function validateWritable(string $path): void
{
if (!is_writable($path)) {
throw FilePermissionException::write($path, 'Path is not writable');
}
}
/**
* Validiert ob Pfad lesbar ist
*
* @throws FilePermissionException
*/
public function validateReadable(string $path): void
{
if (!is_readable($path)) {
throw FilePermissionException::read($path, 'Path is not readable');
}
}
/**
* Vollständige Validierung für File-Upload
*
* @throws FilesystemException
*/
public function validateUpload(string $path, FileSize $fileSize): void
{
$this->validatePath($path);
$this->validateExtension($path);
$this->validateFileSize($fileSize);
}
/**
* Vollständige Validierung für File-Read
*
* @throws FilesystemException
*/
public function validateRead(string $path): void
{
$this->validatePath($path);
$this->validateExists($path);
$this->validateReadable($path);
}
/**
* Vollständige Validierung für File-Write
*
* @throws FileValidationException
* @throws DirectoryCreateException
* @throws FilePermissionException
*/
public function validateWrite(string $path, ?FileSize $fileSize = null): void
{
$this->validatePath($path);
$this->validateExtension($path);
if ($fileSize !== null) {
$this->validateFileSize($fileSize);
}
$directory = dirname($path);
if (!is_dir($directory)) {
throw new DirectoryCreateException($directory);
}
$this->validateWritable($directory);
}
/**
* Prüft auf Path Traversal Patterns
*
* Performance: Compiled regex (1 call) statt 6 str_contains() Calls
*/
private function containsPathTraversal(string $path): bool
{
return preg_match($this->pathTraversalPattern, $path) === 1;
}
/**
* Gibt erlaubte Extensions zurück
*
* @return array<string>|null
*/
public function getAllowedExtensions(): ?array
{
return $this->allowedExtensions;
}
/**
* Gibt blockierte Extensions zurück
*
* @return array<string>|null
*/
public function getBlockedExtensions(): ?array
{
return $this->blockedExtensions;
}
/**
* Gibt maximale Dateigröße zurück
*/
public function getMaxFileSize(): ?FileSize
{
return $this->maxFileSize;
}
/**
* Prüft ob Extension erlaubt ist (ohne Exception)
*/
public function isExtensionAllowed(string $extension): bool
{
$extension = strtolower(ltrim($extension, '.'));
// O(1) lookup mit optimierten Maps
if ($this->allowedExtensionsMap !== null) {
return isset($this->allowedExtensionsMap[$extension]);
}
if ($this->blockedExtensionsMap !== null) {
return !isset($this->blockedExtensionsMap[$extension]);
}
return true; // Keine Einschränkungen
}
}