4cc3f5bee4
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Successful in 5s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Generic: Repo Health / Release configuration (push) Successful in 5s
Generic: Repo Health / Scripts governance (push) Successful in 5s
Generic: Repo Health / Release configuration (pull_request) Successful in 6s
Generic: Repo Health / Scripts governance (pull_request) Successful in 6s
Generic: Repo Health / Repository health (push) Successful in 14s
Generic: Repo Health / Repository health (pull_request) Successful in 12s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 44s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 49s
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been skipped
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been skipped
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
- Convert tabs to spaces (3,413 violations) - Fix line endings, trailing whitespace, brace placement - Break lines exceeding 150-char absolute limit - Replace heredoc tab closers with spaces - Fix empty elseif, forbidden function calls - Update phpcs.xml: exclude rules inappropriate for CLI scripts (SideEffects, MissingNamespace, MultipleClasses, HeaderOrder, empty catch blocks) Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
512 lines
15 KiB
PHP
512 lines
15 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
*
|
|
* This file is part of a Moko Consulting project.
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*
|
|
* FILE INFORMATION
|
|
* DEFGROUP: MokoStandards.Enterprise.Validation
|
|
* INGROUP: MokoStandards.Enterprise
|
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
* PATH: /lib/Enterprise/InputValidator.php
|
|
* BRIEF: Input validation library
|
|
*/
|
|
|
|
/**
|
|
* Input Validation Library - Security-focused input validation and sanitization.
|
|
*
|
|
* This class provides comprehensive validation to prevent:
|
|
* - Path traversal attacks
|
|
* - Shell injection
|
|
* - SQL injection
|
|
* - XSS attacks
|
|
* - Invalid data types
|
|
* - Out-of-range values
|
|
*
|
|
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*
|
|
* @package MokoStandards\Enterprise
|
|
* @version 04.00.04
|
|
* @author MokoStandards Team
|
|
* @license GPL-3.0-or-later
|
|
*/
|
|
|
|
namespace MokoEnterprise;
|
|
|
|
use InvalidArgumentException;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* Exception raised when validation fails.
|
|
*/
|
|
class ValidationError extends RuntimeException
|
|
{
|
|
}
|
|
|
|
/**
|
|
* Input validation and sanitization utilities.
|
|
*
|
|
* Features:
|
|
* - Path validation (prevent path traversal)
|
|
* - Version format validation (semver, Moko format)
|
|
* - Email validation
|
|
* - URL validation with scheme checking
|
|
* - Shell injection prevention
|
|
* - SQL injection prevention
|
|
* - Integer validation with range checking
|
|
* - String validation with length/pattern checking
|
|
* - Choice validation (enum-like)
|
|
*
|
|
* Example:
|
|
* ```php
|
|
* use MokoEnterprise\InputValidator;
|
|
*
|
|
* $path = InputValidator::validatePath('/tmp/file.txt');
|
|
* $email = InputValidator::validateEmail('user@example.com');
|
|
* $version = InputValidator::validateVersion('04.00.04', 'moko');
|
|
* $safe = InputValidator::sanitizeShellInput('user; rm -rf /');
|
|
* ```
|
|
*/
|
|
class InputValidator
|
|
{
|
|
public const VERSION = '04.06.00';
|
|
|
|
/**
|
|
* Validate and sanitize file paths to prevent path traversal.
|
|
*
|
|
* @param string $path Path to validate
|
|
* @param bool $allowRelative Allow relative paths
|
|
* @param bool $mustExist Path must exist
|
|
* @param array<string>|null $allowedExtensions List of allowed file extensions
|
|
* @return string Validated path
|
|
* @throws ValidationError If path is invalid or dangerous
|
|
*/
|
|
public static function validatePath(
|
|
string $path,
|
|
bool $allowRelative = false,
|
|
bool $mustExist = false,
|
|
?array $allowedExtensions = null
|
|
): string {
|
|
if (empty($path)) {
|
|
throw new ValidationError("Path must be a non-empty string");
|
|
}
|
|
|
|
// Check for path traversal attempts
|
|
if (strpos($path, '..') !== false) {
|
|
throw new ValidationError("Path traversal detected (..)");
|
|
}
|
|
|
|
// Resolve to absolute path if not allowing relative
|
|
if (!$allowRelative) {
|
|
$realPath = realpath($path);
|
|
if ($realPath === false && $mustExist) {
|
|
throw new ValidationError("Path does not exist: {$path}");
|
|
}
|
|
if ($realPath !== false) {
|
|
$path = $realPath;
|
|
}
|
|
}
|
|
|
|
// Check if path must exist
|
|
if ($mustExist && !file_exists($path)) {
|
|
throw new ValidationError("Path does not exist: {$path}");
|
|
}
|
|
|
|
// Check file extension if specified
|
|
if ($allowedExtensions !== null) {
|
|
$extension = pathinfo($path, PATHINFO_EXTENSION);
|
|
if ($extension !== '') {
|
|
$allowedLower = array_map('strtolower', $allowedExtensions);
|
|
if (!in_array(strtolower($extension), $allowedLower, true)) {
|
|
throw new ValidationError(
|
|
"Invalid file extension: .{$extension}. " .
|
|
"Allowed: " . implode(', ', $allowedExtensions)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $path;
|
|
}
|
|
|
|
/**
|
|
* Validate version strings.
|
|
*
|
|
* @param string $version Version string to validate
|
|
* @param string $formatType Version format ('semver', 'simple', 'moko')
|
|
* @return string Validated version string
|
|
* @throws ValidationError If version format is invalid
|
|
*/
|
|
public static function validateVersion(string $version, string $formatType = 'semver'): string
|
|
{
|
|
if (empty($version)) {
|
|
throw new ValidationError("Version must be a non-empty string");
|
|
}
|
|
|
|
switch ($formatType) {
|
|
case 'semver':
|
|
// Semantic versioning: MAJOR.MINOR.PATCH
|
|
$pattern = '/^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$/';
|
|
if (!preg_match($pattern, $version)) {
|
|
throw new ValidationError(
|
|
"Invalid semver format: {$version}. Expected: MAJOR.MINOR.PATCH"
|
|
);
|
|
}
|
|
break;
|
|
|
|
case 'moko':
|
|
// MokoStandards format: XX.YY.ZZ
|
|
$pattern = '/^\d{2}\.\d{2}\.\d{2}$/';
|
|
if (!preg_match($pattern, $version)) {
|
|
throw new ValidationError(
|
|
"Invalid MokoStandards version format: {$version}. Expected: XX.YY.ZZ"
|
|
);
|
|
}
|
|
break;
|
|
|
|
case 'simple':
|
|
// Simple format: X.Y or X.Y.Z
|
|
$pattern = '/^\d+\.\d+(\.\d+)?$/';
|
|
if (!preg_match($pattern, $version)) {
|
|
throw new ValidationError("Invalid version format: {$version}");
|
|
}
|
|
break;
|
|
|
|
default:
|
|
throw new ValidationError("Unknown version format type: {$formatType}");
|
|
}
|
|
|
|
return $version;
|
|
}
|
|
|
|
/**
|
|
* Validate email addresses.
|
|
*
|
|
* @param string $email Email address to validate
|
|
* @return string Validated email address (lowercase)
|
|
* @throws ValidationError If email is invalid
|
|
*/
|
|
public static function validateEmail(string $email): string
|
|
{
|
|
if (empty($email)) {
|
|
throw new ValidationError("Email must be a non-empty string");
|
|
}
|
|
|
|
// Simple but effective email regex
|
|
$pattern = '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/';
|
|
if (!preg_match($pattern, $email)) {
|
|
throw new ValidationError("Invalid email format: {$email}");
|
|
}
|
|
|
|
return strtolower($email);
|
|
}
|
|
|
|
/**
|
|
* Validate URLs and check schemes.
|
|
*
|
|
* @param string $url URL to validate
|
|
* @param array<string>|null $allowedSchemes List of allowed URL schemes (e.g., ['http', 'https'])
|
|
* @return string Validated URL
|
|
* @throws ValidationError If URL is invalid
|
|
*/
|
|
public static function validateUrl(string $url, ?array $allowedSchemes = null): string
|
|
{
|
|
if (empty($url)) {
|
|
throw new ValidationError("URL must be a non-empty string");
|
|
}
|
|
|
|
$parsed = parse_url($url);
|
|
if ($parsed === false || !isset($parsed['scheme']) || !isset($parsed['host'])) {
|
|
throw new ValidationError("Invalid URL format: {$url}");
|
|
}
|
|
|
|
if ($allowedSchemes !== null && !in_array($parsed['scheme'], $allowedSchemes, true)) {
|
|
throw new ValidationError(
|
|
"URL scheme '{$parsed['scheme']}' not allowed. " .
|
|
"Allowed: " . implode(', ', $allowedSchemes)
|
|
);
|
|
}
|
|
|
|
return $url;
|
|
}
|
|
|
|
/**
|
|
* Sanitize input to prevent shell injection.
|
|
*
|
|
* @param string $input Input string to sanitize
|
|
* @return string Sanitized string
|
|
*/
|
|
public static function sanitizeShellInput(string $input): string
|
|
{
|
|
// Remove dangerous shell characters
|
|
$dangerousChars = [';', '&', '|', '`', '$', '(', ')', '<', '>', "\n", "\r"];
|
|
$sanitized = str_replace($dangerousChars, '', $input);
|
|
|
|
return trim($sanitized);
|
|
}
|
|
|
|
/**
|
|
* Sanitize input to prevent SQL injection.
|
|
*
|
|
* @param string $input Input string to sanitize
|
|
* @return string Sanitized string
|
|
*/
|
|
public static function sanitizeSqlInput(string $input): string
|
|
{
|
|
// Remove SQL injection patterns
|
|
$dangerousPatterns = ["'", '"', '--', '/*', '*/', 'xp_', 'sp_'];
|
|
$sanitized = str_replace($dangerousPatterns, '', $input);
|
|
|
|
return trim($sanitized);
|
|
}
|
|
|
|
/**
|
|
* Validate and convert to integer with range checking.
|
|
*
|
|
* @param mixed $value Value to validate
|
|
* @param int|null $minValue Minimum allowed value
|
|
* @param int|null $maxValue Maximum allowed value
|
|
* @return int Validated integer
|
|
* @throws ValidationError If value is invalid or out of range
|
|
*/
|
|
public static function validateInteger(
|
|
mixed $value,
|
|
?int $minValue = null,
|
|
?int $maxValue = null
|
|
): int {
|
|
if (!is_numeric($value)) {
|
|
throw new ValidationError("Cannot convert to integer: {$value}");
|
|
}
|
|
|
|
$intValue = (int) $value;
|
|
|
|
if ($minValue !== null && $intValue < $minValue) {
|
|
throw new ValidationError("Value {$intValue} is below minimum {$minValue}");
|
|
}
|
|
|
|
if ($maxValue !== null && $intValue > $maxValue) {
|
|
throw new ValidationError("Value {$intValue} is above maximum {$maxValue}");
|
|
}
|
|
|
|
return $intValue;
|
|
}
|
|
|
|
/**
|
|
* Validate string with length and pattern checking.
|
|
*
|
|
* @param string $value String to validate
|
|
* @param int|null $minLength Minimum string length
|
|
* @param int|null $maxLength Maximum string length
|
|
* @param string|null $pattern Regex pattern to match
|
|
* @return string Validated string
|
|
* @throws ValidationError If string is invalid
|
|
*/
|
|
public static function validateString(
|
|
string $value,
|
|
?int $minLength = null,
|
|
?int $maxLength = null,
|
|
?string $pattern = null
|
|
): string {
|
|
$length = strlen($value);
|
|
|
|
if ($minLength !== null && $length < $minLength) {
|
|
throw new ValidationError("String length {$length} is below minimum {$minLength}");
|
|
}
|
|
|
|
if ($maxLength !== null && $length > $maxLength) {
|
|
throw new ValidationError("String length {$length} exceeds maximum {$maxLength}");
|
|
}
|
|
|
|
if ($pattern !== null && !preg_match($pattern, $value)) {
|
|
throw new ValidationError("String does not match pattern: {$pattern}");
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* Validate that value is in a list of allowed choices.
|
|
*
|
|
* @param mixed $value Value to validate
|
|
* @param array<mixed> $choices List of allowed values
|
|
* @return mixed Validated value
|
|
* @throws ValidationError If value not in choices
|
|
*/
|
|
public static function validateChoice(mixed $value, array $choices): mixed
|
|
{
|
|
if (!in_array($value, $choices, true)) {
|
|
$choicesStr = implode(', ', array_map('strval', $choices));
|
|
throw new ValidationError("Invalid choice: {$value}. Allowed: {$choicesStr}");
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Chainable validator for complex validation scenarios.
|
|
*
|
|
* Features:
|
|
* - Fluent interface for chaining validations
|
|
* - Accumulates errors instead of throwing immediately
|
|
* - Single validation call at the end
|
|
*
|
|
* Example:
|
|
* ```php
|
|
* $validator = new Validator('user@example.com', 'email');
|
|
* $email = $validator
|
|
* ->isString(minLength: 5, maxLength: 100)
|
|
* ->isEmail()
|
|
* ->validate();
|
|
* ```
|
|
*/
|
|
class Validator
|
|
{
|
|
private mixed $value;
|
|
private string $name;
|
|
/** @var array<string> */
|
|
private array $errors = [];
|
|
|
|
/**
|
|
* Initialize validator.
|
|
*
|
|
* @param mixed $value Value to validate
|
|
* @param string $name Name of the value (for error messages)
|
|
*/
|
|
public function __construct(mixed $value, string $name = 'value')
|
|
{
|
|
$this->value = $value;
|
|
$this->name = $name;
|
|
}
|
|
|
|
/**
|
|
* Check if value is a string.
|
|
*
|
|
* @param int|null $minLength Minimum length
|
|
* @param int|null $maxLength Maximum length
|
|
* @return self
|
|
*/
|
|
public function isString(?int $minLength = null, ?int $maxLength = null): self
|
|
{
|
|
try {
|
|
if (!is_string($this->value)) {
|
|
throw new ValidationError("Value must be a string");
|
|
}
|
|
InputValidator::validateString($this->value, $minLength, $maxLength);
|
|
} catch (ValidationError $e) {
|
|
$this->errors[] = $e->getMessage();
|
|
}
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Check if value is an integer.
|
|
*
|
|
* @param int|null $minValue Minimum value
|
|
* @param int|null $maxValue Maximum value
|
|
* @return self
|
|
*/
|
|
public function isInteger(?int $minValue = null, ?int $maxValue = null): self
|
|
{
|
|
try {
|
|
InputValidator::validateInteger($this->value, $minValue, $maxValue);
|
|
} catch (ValidationError $e) {
|
|
$this->errors[] = $e->getMessage();
|
|
}
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Check if value is a valid email.
|
|
*
|
|
* @return self
|
|
*/
|
|
public function isEmail(): self
|
|
{
|
|
try {
|
|
if (!is_string($this->value)) {
|
|
throw new ValidationError("Email must be a string");
|
|
}
|
|
InputValidator::validateEmail($this->value);
|
|
} catch (ValidationError $e) {
|
|
$this->errors[] = $e->getMessage();
|
|
}
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Check if value is a valid URL.
|
|
*
|
|
* @param array<string>|null $allowedSchemes Allowed URL schemes
|
|
* @return self
|
|
*/
|
|
public function isUrl(?array $allowedSchemes = null): self
|
|
{
|
|
try {
|
|
if (!is_string($this->value)) {
|
|
throw new ValidationError("URL must be a string");
|
|
}
|
|
InputValidator::validateUrl($this->value, $allowedSchemes);
|
|
} catch (ValidationError $e) {
|
|
$this->errors[] = $e->getMessage();
|
|
}
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Check if value matches regex pattern.
|
|
*
|
|
* @param string $pattern Regex pattern
|
|
* @return self
|
|
*/
|
|
public function matches(string $pattern): self
|
|
{
|
|
if (!preg_match($pattern, (string) $this->value)) {
|
|
$this->errors[] = "{$this->name} does not match pattern: {$pattern}";
|
|
}
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Perform validation and raise exception if errors found.
|
|
*
|
|
* @return mixed The validated value
|
|
* @throws ValidationError If validation failed
|
|
*/
|
|
public function validate(): mixed
|
|
{
|
|
if (!empty($this->errors)) {
|
|
$errorMsg = "Validation failed for {$this->name}:\n";
|
|
$errorMsg .= implode("\n", array_map(fn($e) => " - {$e}", $this->errors));
|
|
throw new ValidationError($errorMsg);
|
|
}
|
|
return $this->value;
|
|
}
|
|
|
|
/**
|
|
* Get all validation errors.
|
|
*
|
|
* @return array<string>
|
|
*/
|
|
public function getErrors(): array
|
|
{
|
|
return $this->errors;
|
|
}
|
|
|
|
/**
|
|
* Check if validation has errors.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function hasErrors(): bool
|
|
{
|
|
return !empty($this->errors);
|
|
}
|
|
}
|