validate($value, $constraints); } /** * Specifically validate a Host ID (numeric and >= 0). * * @param mixed $value The value to validate. * * @return bool True if the value is a valid host id, false otherwise. */ public static function isValidHostId(mixed $value): bool { return self::isValid($value, [ new Assert\NotNull(), new Assert\Type('numeric'), new Assert\GreaterThanOrEqual(0), ]); } /** * Specifically validate an RRD path to prevent traversal. * * When $rraRoot is supplied, the path is also checked against the real * resolved root to defeat symlink-based escapes. The path must already * exist on disk for this branch to succeed; callers validating a * not-yet-created file should pass $rraRoot = null and check * containment after creation. * * @param string $path The relative RRD path to validate. * @param string|null $rraRoot Optional real RRA root to enforce containment against. * * @return bool True if the path is safe and (when $rraRoot is set) contained, false otherwise. */ public static function isValidRrdPath(string $path, ?string $rraRoot = null): bool { if ($path === '' || strpos($path, "\0") !== false) { return false; } if (!preg_match('/^[a-zA-Z0-9_\-\.\/]+$/', $path)) { return false; } if (str_contains($path, '..')) { return false; } if ($path[0] === '/') { return false; } if ($rraRoot !== null) { $realRoot = realpath($rraRoot); if ($realRoot === false) { return false; } // Find the deepest existing parent directory to validate containment // for files that don't exist yet. $testPath = $rraRoot . DIRECTORY_SEPARATOR . $path; $current = $testPath; while (!file_exists($current) && $current !== dirname($current)) { $current = dirname($current); } $realPath = realpath($current); if ($realPath === false) { return false; } if ($realPath !== $realRoot && strncmp($realPath, $realRoot . DIRECTORY_SEPARATOR, strlen($realRoot) + 1) !== 0) { return false; } } return true; } /** * Validate an IP address (v4 or v6). * * @param string $ip The IP address to validate. * * @return bool True if the value is a valid IPv4 or IPv6 address, false otherwise. */ public static function isValidIpAddress(string $ip): bool { return self::isValid($ip, [ new Assert\Ip(['version' => 'all']), ]); } /** * Validate an email address (HTML5 syntax, max 254 chars). * * @param string $email The email address to validate. * * @return bool True if the value is a valid email address, false otherwise. */ public static function isValidEmail(string $email): bool { return self::isValid($email, [ new Assert\Email(mode: 'html5'), new Assert\Length(max: 254), ]); } /** * Validate an SNMP community string (non-blank, max 255 chars, printable ASCII only). * * @param string $community The SNMP community string to validate. * * @return bool True if the value is a valid community string, false otherwise. */ public static function isValidSnmpCommunity(string $community): bool { return self::isValid($community, [ new Assert\NotBlank(), new Assert\Length(max: 255), new Assert\Regex('/\A[\x20-\x7E]+\z/'), ]); } /** Apply the same axis validation to graph, template and aggregate saves. */ public static function validateRrdGraphAxisInput(mixed $value, string $name): mixed { return self::validateInput($value, $name, [new Assert\Callback( static function (mixed $value, \Symfony\Component\Validator\Context\ExecutionContextInterface $context) use ($name): void { if (!rrd_graph_axis_option_is_valid($name, $value)) { $context->buildViolation(__('Invalid RRDtool axis option.'))->addViolation(); } } )], 3); } /** * Common input validation helper. * Populates $_SESSION with the value and any error states. * * @param mixed $value The value to validate. * @param string $name The field name (used in $_SESSION). * @param array $constraints The Symfony Validator constraints. * @param mixed $message_id The Cacti message ID to raise on failure. * * @return mixed The original value. */ public static function validateInput(mixed $value, string $name, array $constraints, mixed $message_id = 3): mixed { $_SESSION[SESS_FIELD_VALUES][$name] = $value; $violations = self::validate($value, $constraints); if ($violations->count() > 0) { $_SESSION[SESS_ERROR_FIELDS][$name] = $message_id; raise_message($message_id); } return $value; } }