20) { return self::createValidationResult(false, 'Nickname must be 20 characters or less'); } // Check allowed characters (alphanumeric, spaces, hyphens, apostrophes) if (!preg_match('/^[a-zA-Z0-9\s\'-]+$/', $trimmed)) { return self::createValidationResult(false, 'Nickname can only contain letters, numbers, spaces, hyphens, and apostrophes'); } // Sanitize $sanitized = Security::sanitizeInput($trimmed); return self::createValidationResult(true, 'Valid nickname', $sanitized); } /** * Validate and sanitize chat message */ public static function validateMessage($message, $maxLength = 1000) { if (!is_string($message)) { return self::createValidationResult(false, 'Message must be a string'); } $trimmed = trim($message); // Check length if (strlen($trimmed) < 1) { return self::createValidationResult(false, 'Message cannot be empty'); } if (strlen($trimmed) > $maxLength) { return self::createValidationResult(false, 'Message exceeds maximum length of ' . $maxLength . ' characters'); } // Basic XSS check (additional to sanitization) $xssPatterns = [ '/]*>.*?<\/script>/is', '/javascript:/i', '/on\w+\s*=/i', '/]*>.*?<\/iframe>/is' ]; foreach ($xssPatterns as $pattern) { if (preg_match($pattern, $trimmed)) { return self::createValidationResult(false, 'Message contains potentially harmful content'); } } // Sanitize $sanitized = Security::sanitizeInput($trimmed); return self::createValidationResult(true, 'Valid message', $sanitized); } /** * Validate user ID format */ public static function validateUserId($userId) { if (!is_string($userId)) { return self::createValidationResult(false, 'User ID must be a string'); } // Should be hexadecimal (from bin2hex) if (!preg_match('/^[a-f0-9]+$/', $userId)) { return self::createValidationResult(false, 'Invalid user ID format'); } // Check length (16 characters for 8 bytes) if (strlen($userId) !== 16) { return self::createValidationResult(false, 'Invalid user ID length'); } return self::createValidationResult(true, 'Valid user ID', $userId); } /** * Validate admin login credentials */ public static function validateAdminLogin($username, $password) { // Username validation if (empty($username) || !is_string($username)) { return self::createValidationResult(false, 'Username is required'); } $username = trim($username); if (strlen($username) < 3 || strlen($username) > 50) { return self::createValidationResult(false, 'Username must be between 3-50 characters'); } if (!preg_match('/^[a-zA-Z0-9_-]+$/', $username)) { return self::createValidationResult(false, 'Username can only contain letters, numbers, hyphens, and underscores'); } // Password validation if (empty($password) || !is_string($password)) { return self::createValidationResult(false, 'Password is required'); } $password = trim($password); if (strlen($password) < 6) { return self::createValidationResult(false, 'Password must be at least 6 characters long'); } // Additional password strength checks (optional) $hasUppercase = preg_match('/[A-Z]/', $password); $hasLowercase = preg_match('/[a-z]/', $password); $hasNumbers = preg_match('/[0-9]/', $password); if (!$hasUppercase || !$hasLowercase || !$hasNumbers) { return self::createValidationResult(false, 'Password must contain at least one uppercase letter, one lowercase letter, and one number'); } return self::createValidationResult(true, 'Credentials format valid', [ 'username' => $username, 'password' => $password ]); } /** * Validate API request parameters */ public static function validateApiRequest($params, $rules) { $validated = []; $errors = []; foreach ($rules as $field => $rule) { $value = $params[$field] ?? null; // Check required fields if (isset($rule['required']) && $rule['required'] && $value === null) { $errors[$field] = $rule['required_message'] ?? "{$field} is required"; continue; } // Skip validation if not required and not provided if ($value === null) { continue; } // Type validation if (isset($rule['type'])) { $valid = self::validateType($value, $rule['type'], $rule); if (!$valid['valid']) { $errors[$field] = $valid['message']; continue; } $validated[$field] = $valid['value']; continue; } // Apply sanitization if (isset($rule['sanitize'])) { $value = Security::sanitizeInput($value, $rule['sanitize']); } $validated[$field] = $value; } return [ 'valid' => empty($errors), 'validated' => $validated, 'errors' => $errors ]; } /** * Validate value against specific type */ private static function validateType($value, $type, $rule = []) { switch ($type) { case 'string': if (!is_string($value)) { return ['valid' => false, 'message' => 'Must be a string']; } $value = trim($value); if (isset($rule['min_length']) && strlen($value) < $rule['min_length']) { return ['valid' => false, 'message' => "Must be at least {$rule['min_length']} characters long"]; } if (isset($rule['max_length']) && strlen($value) > $rule['max_length']) { return ['valid' => false, 'message' => "Must be no more than {$rule['max_length']} characters long"]; } if (isset($rule['pattern']) && !preg_match($rule['pattern'], $value)) { return ['valid' => false, 'message' => $rule['pattern_message'] ?? 'Invalid format']; } return ['valid' => true, 'value' => $value]; case 'int': $intVal = filter_var($value, FILTER_VALIDATE_INT); if ($intVal === false) { return ['valid' => false, 'message' => 'Must be a valid integer']; } if (isset($rule['min']) && $intVal < $rule['min']) { return ['valid' => false, 'message' => "Must be at least {$rule['min']}"]; } if (isset($rule['max']) && $intVal > $rule['max']) { return ['valid' => false, 'message' => "Must be no more than {$rule['max']}"]; } return ['valid' => true, 'value' => $intVal]; case 'email': $email = filter_var(trim($value), FILTER_VALIDATE_EMAIL); if (!$email) { return ['valid' => false, 'message' => 'Invalid email address']; } return ['valid' => true, 'value' => $email]; case 'boolean': $boolVal = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); if ($boolVal === null) { return ['valid' => false, 'message' => 'Must be a boolean value']; } return ['valid' => true, 'value' => $boolVal]; default: return ['valid' => true, 'value' => $value]; } } /** * Validate heartbeat data */ public static function validateHeartbeat($data) { $rules = [ 'nickname' => [ 'required' => false, 'type' => 'string', 'max_length' => 20, 'pattern' => '/^[a-zA-Z0-9\s\'-]*$/', 'pattern_message' => 'Nickname can only contain letters, numbers, spaces, hyphens, and apostrophes' ] ]; return self::validateApiRequest($data, $rules); } /** * Validate message send request */ public static function validateMessageSend($data) { $rules = [ 'nickname' => [ 'required' => true, 'type' => 'string', 'min_length' => 1, 'max_length' => 20, 'pattern' => '/^[a-zA-Z0-9\s\'-]+$/', 'pattern_message' => 'Nickname can only contain letters, numbers, spaces, hyphens, and apostrophes', 'required_message' => 'Nickname is required' ], 'message' => [ 'required' => true, 'type' => 'string', 'min_length' => 1, 'max_length' => 1000, 'required_message' => 'Message cannot be empty' ] ]; return self::validateApiRequest($data, $rules); } /** * Create validation result array */ private static function createValidationResult($valid, $message, $data = null) { $result = [ 'valid' => $valid, 'message' => $message ]; if ($data !== null) { $result['data'] = $data; } return $result; } }