InputValidator.php

19.20 KB
31/07/2025 12:05
PHP
InputValidator.php
<?php

/**
 * InputValidator class for comprehensive input validation
 * Provides validation methods for all API endpoints
 */
class InputValidator
{
    /**
     * @var mixed
     */
    private $errorHandler;

    /**
     * Constructor
     * @param ErrorHandler $errorHandler Error handler instance
     */
    public function __construct($errorHandler)
    {
        $this->errorHandler = $errorHandler;
    }

    /**
     * Validate album creation input
     * @param array $input Input data
     * @return array|null Returns error response if validation fails, null if valid
     */
    public function validateAlbumCreation($input)
    {
        // Check required fields
        $error = $this->errorHandler->validateRequiredFields($input, ['title']);
        if ($error) {
            return $error;
        }

        // Validate title
        $title = trim($input['title']);
        $error = $this->errorHandler->validateStringLength($title, 'title', 1, 255);
        if ($error) {
            return $error;
        }

        // Validate description if provided
        if (isset($input['description'])) {
            $description = trim($input['description']);
            $error = $this->errorHandler->validateStringLength($description, 'description', 0, 1000);
            if ($error) {
                return $error;
            }

        }

        return null;
    }

    /**
     * Validate album update input
     * @param array $input Input data
     * @return array|null Returns error response if validation fails, null if valid
     */
    public function validateAlbumUpdate($input)
    {
        // At least one field must be provided
        $allowedFields = ['title', 'description', 'is_rss_enabled'];
        $hasValidField = false;

        foreach ($allowedFields as $field) {
            if (isset($input[$field])) {
                $hasValidField = true;
                break;
            }
        }

        if (!$hasValidField) {
            return $this->errorHandler->createErrorResponse(
                'At least one field must be provided for update: '.implode(', ', $allowedFields),
                'INVALID_INPUT',
                ['allowed_fields' => $allowedFields]
            );
        }

        // Validate title if provided
        if (isset($input['title'])) {
            $title = trim($input['title']);
            if (empty($title)) {
                return $this->errorHandler->createErrorResponse(
                    'Album title cannot be empty',
                    'VALIDATION_FAILED',
                    ['field' => 'title']
                );
            }
            $error = $this->errorHandler->validateStringLength($title, 'title', 1, 255);
            if ($error) {
                return $error;
            }

        }

        // Validate description if provided
        if (isset($input['description'])) {
            $description = trim($input['description']);
            $error = $this->errorHandler->validateStringLength($description, 'description', 0, 1000);
            if ($error) {
                return $error;
            }

        }

        // Validate is_rss_enabled if provided
        if (isset($input['is_rss_enabled'])) {
            if (!is_bool($input['is_rss_enabled']) && !in_array($input['is_rss_enabled'], [0, 1, '0', '1', 'true', 'false'], true)) {
                return $this->errorHandler->createErrorResponse(
                    'is_rss_enabled must be a boolean value',
                    'VALIDATION_FAILED',
                    ['field' => 'is_rss_enabled', 'value' => $input['is_rss_enabled']]
                );
            }
        }

        return null;
    }

    /**
     * Validate tag creation input
     * @param array $input Input data
     * @return array|null Returns error response if validation fails, null if valid
     */
    public function validateTagCreation($input)
    {
        // Check required fields
        $error = $this->errorHandler->validateRequiredFields($input, ['name']);
        if ($error) {
            return $error;
        }

        // Validate name
        $name = trim($input['name']);
        $error = $this->errorHandler->validateStringLength($name, 'name', 1, 100);
        if ($error) {
            return $error;
        }

        // Validate color if provided
        if (isset($input['color'])) {
            $color = trim($input['color']);
            if (!preg_match('/^#[0-9A-Fa-f]{6}$/', $color)) {
                return $this->errorHandler->createErrorResponse(
                    'Color must be a valid hex color code (e.g., #3498db)',
                    'VALIDATION_FAILED',
                    ['field' => 'color', 'value' => $color, 'format' => '#RRGGBB']
                );
            }
        }

        return null;
    }

    /**
     * Validate album tags update input
     * @param array $input Input data
     * @return array|null Returns error response if validation fails, null if valid
     */
    public function validateAlbumTagsUpdate($input)
    {
        // Check required fields
        $error = $this->errorHandler->validateRequiredFields($input, ['tagIds']);
        if ($error) {
            return $error;
        }

        // Validate tagIds is an array
        if (!is_array($input['tagIds'])) {
            return $this->errorHandler->createErrorResponse(
                'tagIds must be an array',
                'VALIDATION_FAILED',
                ['field' => 'tagIds', 'type' => gettype($input['tagIds'])]
            );
        }

        // Validate each tag ID is numeric
        foreach ($input['tagIds'] as $index => $tagId) {
            if (!is_numeric($tagId)) {
                return $this->errorHandler->createErrorResponse(
                    "Tag ID at index {$index} must be numeric",
                    'VALIDATION_FAILED',
                    ['field' => "tagIds[{$index}]", 'value' => $tagId]
                );
            }
        }

        return null;
    }

    /**
     * Validate tag ID parameter
     * @param string $albumId Tag ID
     * @return array|null Returns error response if validation fails, null if valid
     */
    public function validateTagId($tagId)
    {
        if (empty($tagId)) {
            return $this->errorHandler->createErrorResponse(
                'Tag ID is required',
                'MISSING_REQUIRED_FIELD',
                ['field' => 'tagId']
            );
        }

        if (!is_numeric($tagId)) {
            return $this->errorHandler->createErrorResponse(
                'Tag ID must be numeric',
                'VALIDATION_FAILED',
                ['field' => 'tagId', 'value' => $tagId]
            );
        }

        return null;
    }

    /**
     * Validate album ID parameter
     * @param string $albumId Album ID
     * @return array|null Returns error response if validation fails, null if valid
     */
    public function validateAlbumId($albumId)
    {
        if (empty($albumId)) {
            return $this->errorHandler->createErrorResponse(
                'Album ID is required',
                'MISSING_REQUIRED_FIELD',
                ['field' => 'albumId']
            );
        }

        if (!is_numeric($albumId)) {
            return $this->errorHandler->createErrorResponse(
                'Album ID must be numeric',
                'VALIDATION_FAILED',
                ['field' => 'albumId', 'value' => $albumId]
            );
        }

        return null;
    }

    /**
     * Validate pagination parameters
     * @param array $params Parameters containing page, limit, etc.
     * @return array|null Returns error response if validation fails, null if valid
     */
    public function validatePaginationParams($params)
    {
        // Validate page
        if (isset($params['page'])) {
            $error = $this->errorHandler->validateNumeric($params['page'], 'page', 1);
            if ($error) {
                return $error;
            }

        }

        // Validate limit
        if (isset($params['limit'])) {
            $error = $this->errorHandler->validateNumeric($params['limit'], 'limit', 1, 100);
            if ($error) {
                return $error;
            }

        }

        // Validate sortBy
        if (isset($params['sortBy'])) {
            $allowedSortFields = ['uploaded_at', 'filename', 'file_size', 'width', 'height', 'created_at', 'updated_at'];
            $error = $this->errorHandler->validateAllowedValues($params['sortBy'], 'sortBy', $allowedSortFields);
            if ($error) {
                return $error;
            }

        }

        // Validate sortOrder
        if (isset($params['sortOrder'])) {
            $allowedSortOrders = ['asc', 'desc'];
            $error = $this->errorHandler->validateAllowedValues($params['sortOrder'], 'sortOrder', $allowedSortOrders);
            if ($error) {
                return $error;
            }

        }

        return null;
    }

    /**
     * Validate file upload parameters
     * @param array $files $_FILES array
     * @param string $albumId Album ID
     * @return array|null Returns error response if validation fails, null if valid
     */
    public function validateFileUpload($files, $albumId)
    {
        // Validate album ID
        $error = $this->validateAlbumId($albumId);
        if ($error) {
            return $error;
        }

        // Check if files were uploaded
        if (empty($files) || !isset($files['photos'])) {
            // Get current PHP upload limits for better error message
            $uploadMaxFilesize = ini_get('upload_max_filesize');
            $postMaxSize = ini_get('post_max_size');
            $maxFileUploads = ini_get('max_file_uploads');

            return $this->errorHandler->createErrorResponse(
                "No files uploaded. Please select valid image files. Current server limits: Max file size: {$uploadMaxFilesize}, Max files: {$maxFileUploads}, Max total size: {$postMaxSize}",
                'INVALID_INPUT',
                [
                    'field' => 'photos',
                    'hint' => 'Files may be too large or in unsupported format',
                    'limits' => [
                        'max_file_size' => $uploadMaxFilesize,
                        'max_files' => $maxFileUploads,
                        'max_total_size' => $postMaxSize
                    ]
                ]
            );
        }

        $photos = $files['photos'];

        // Handle single file upload (convert to array format)
        if (!is_array($photos['name'])) {
            $photos = [
                'name' => [$photos['name']],
                'type' => [$photos['type']],
                'tmp_name' => [$photos['tmp_name']],
                'error' => [$photos['error']],
                'size' => [$photos['size']]
            ];
        }

        // Validate each uploaded file
        for ($i = 0; $i < count($photos['name']); $i++) {
            // Check for upload errors
            if ($photos['error'][$i] !== UPLOAD_ERR_OK) {
                $errorMessage = $this->getUploadErrorMessage($photos['error'][$i]);
                return $this->errorHandler->createErrorResponse(
                    "Upload error for file '{$photos['name'][$i]}': {$errorMessage}",
                    'INVALID_INPUT',
                    [
                        'file' => $photos['name'][$i],
                        'upload_error_code' => $photos['error'][$i],
                        'upload_error_message' => $errorMessage
                    ]
                );
            }

            // Validate file name
            if (empty($photos['name'][$i])) {
                return $this->errorHandler->createErrorResponse(
                    'File name cannot be empty',
                    'INVALID_INPUT',
                    ['file_index' => $i]
                );
            }

            // Validate file size
            if ($photos['size'][$i] <= 0) {
                return $this->errorHandler->createErrorResponse(
                    "File '{$photos['name'][$i]}' is empty or invalid",
                    'INVALID_INPUT',
                    ['file' => $photos['name'][$i], 'size' => $photos['size'][$i]]
                );
            }
        }

        return null;
    }

    /**
     * Validate RSS settings update input
     * @param array $input Input data
     * @return array|null Returns error response if validation fails, null if valid
     */
    public function validateRSSSettingsUpdate($input)
    {
        $allowedFields = ['title', 'description', 'max_items', 'base_url'];
        $hasValidField = false;

        foreach ($allowedFields as $field) {
            if (isset($input[$field])) {
                $hasValidField = true;
                break;
            }
        }

        if (!$hasValidField) {
            return $this->errorHandler->createErrorResponse(
                'At least one field must be provided for RSS settings update: '.implode(', ', $allowedFields),
                'INVALID_INPUT',
                ['allowed_fields' => $allowedFields]
            );
        }

        // Validate title if provided
        if (isset($input['title'])) {
            $error = $this->errorHandler->validateStringLength($input['title'], 'title', 1, 255);
            if ($error) {
                return $error;
            }

        }

        // Validate description if provided
        if (isset($input['description'])) {
            $error = $this->errorHandler->validateStringLength($input['description'], 'description', 0, 1000);
            if ($error) {
                return $error;
            }

        }

        // Validate max_items if provided
        if (isset($input['max_items'])) {
            $error = $this->errorHandler->validateNumeric($input['max_items'], 'max_items', 1, 1000);
            if ($error) {
                return $error;
            }

        }

        // Validate base_url if provided
        if (isset($input['base_url'])) {
            $baseUrl = trim($input['base_url']);
            if (!empty($baseUrl) && !filter_var($baseUrl, FILTER_VALIDATE_URL)) {
                return $this->errorHandler->createErrorResponse(
                    'base_url must be a valid URL',
                    'VALIDATION_FAILED',
                    ['field' => 'base_url', 'value' => $baseUrl]
                );
            }
        }

        return null;
    }

    /**
     * Validate configuration update input
     * @param array $input Input data
     * @return array|null Returns error response if validation fails, null if valid
     */
    public function validateConfigUpdate($input)
    {
        if (empty($input) || !is_array($input)) {
            return $this->errorHandler->createErrorResponse(
                'Configuration data must be provided as an object',
                'INVALID_INPUT'
            );
        }

        // Define allowed configuration keys and their validation rules
        $configRules = [
            'upload_max_file_size' => ['type' => 'numeric', 'min' => 1024, 'max' => 104857600], // 1KB to 100MB
            'image_webp_quality' => ['type' => 'numeric', 'min' => 1, 'max' => 100],
            'image_max_width' => ['type' => 'numeric', 'min' => 100, 'max' => 10000],
            'image_max_height' => ['type' => 'numeric', 'min' => 100, 'max' => 10000],
            'image_thumbnail_size' => ['type' => 'numeric', 'min' => 50, 'max' => 1000],
            'rss_title' => ['type' => 'string', 'min_length' => 1, 'max_length' => 255],
            'rss_description' => ['type' => 'string', 'min_length' => 0, 'max_length' => 1000],
            'rss_max_items' => ['type' => 'numeric', 'min' => 1, 'max' => 1000],
            'rss_base_url' => ['type' => 'url'],
            'storage_max_total_size' => ['type' => 'numeric', 'min' => 1048576], // Minimum 1MB
            'storage_cleanup_enabled' => ['type' => 'boolean']
        ];

        // Validate each provided configuration key
        foreach ($input as $key => $value) {
            if (!isset($configRules[$key])) {
                return $this->errorHandler->createErrorResponse(
                    "Unknown configuration key: {$key}",
                    'VALIDATION_FAILED',
                    ['field' => $key, 'allowed_keys' => array_keys($configRules)]
                );
            }

            $rule = $configRules[$key];

            // Validate based on type
            switch ($rule['type']) {
                case 'numeric':
                    $error = $this->errorHandler->validateNumeric(
                        $value,
                        $key,
                        $rule['min'] ?? null,
                        $rule['max'] ?? null
                    );
                    if ($error) {
                        return $error;
                    }

                    break;

                case 'string':
                    if (!is_string($value)) {
                        return $this->errorHandler->createErrorResponse(
                            "{$key} must be a string",
                            'VALIDATION_FAILED',
                            ['field' => $key, 'type' => gettype($value)]
                        );
                    }
                    $error = $this->errorHandler->validateStringLength(
                        $value,
                        $key,
                        $rule['min_length'] ?? 0,
                        $rule['max_length'] ?? null
                    );
                    if ($error) {
                        return $error;
                    }

                    break;

                case 'boolean':
                    if (!is_bool($value) && !in_array($value, [0, 1, '0', '1', 'true', 'false'], true)) {
                        return $this->errorHandler->createErrorResponse(
                            "{$key} must be a boolean value",
                            'VALIDATION_FAILED',
                            ['field' => $key, 'value' => $value]
                        );
                    }
                    break;

                case 'url':
                    if (!empty($value) && !filter_var($value, FILTER_VALIDATE_URL)) {
                        return $this->errorHandler->createErrorResponse(
                            "{$key} must be a valid URL",
                            'VALIDATION_FAILED',
                            ['field' => $key, 'value' => $value]
                        );
                    }
                    break;
            }
        }

        return null;
    }

    /**
     * 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';
        }
    }
}