- 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.
363 lines
10 KiB
PHP
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
|
|
}
|
|
}
|