<?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';
}
}
}