MD_STD/src/MD_STD_IN.php

518 lines
15 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?PHP
/**
* Gathers wrappers for handling inputs.
*/
declare(strict_types = 1);
/**
* Encapsulates functions for handling inputs.
*/
final class MD_STD_IN {
/**
* Validates and sanitizes input integers to be in line with MySQL
* autoincrement IDs.
*
* @param mixed $input Input string.
*
* @return positive-int
*/
public static function sanitize_id(mixed $input):int {
$input = \filter_var($input, \FILTER_VALIDATE_INT, [
'options' => [
'min_range' => 1, // Minimum number of an ID generated.
'max_range' => 4294967295 // Max value for MySQL's int data type
],
]
);
if (!$input) {
throw new MDpageParameterNotNumericException("Value is not numeric.");
}
return $input;
}
/**
* Sanitizes and validates input integers to be either valid IDs or 0.
*
* @param mixed $input Input string.
*
* @return 0|positive-int
*/
public static function sanitize_id_or_zero(mixed $input):int {
if ($input === "" || $input === 0) {
return 0;
}
$input = \filter_var($input, \FILTER_VALIDATE_INT, [
'options' => [
'min_range' => 0, // Minimum number of an ID generated.
'max_range' => 4294967295 // Max value for MySQL's int data type
],
]
);
if ($input === false) {
throw new MDpageParameterNotNumericException("Value is not numeric.");
}
return $input;
}
/**
* General string sanitization for all purposes. For use of inputs with MySQL's
* MATCH AGAINST, use the dedicated sanitization function.
*
* @param mixed $input Input string.
*
* @return string
*/
public static function sanitize_text(mixed $input):string {
$output = \filter_var($input, FILTER_UNSAFE_RAW);
if ($output === false) {
return "";
}
$output = strip_tags($output);
while (strpos($output, " ") !== false) {
$output = str_replace(" ", " ", $output);
}
return trim($output);
}
/**
* String sanitization for 3 o4 6 characters RGB color codes (sans the leading #).
*
* @param mixed $input Input string.
*
* @return non-empty-string
*/
public static function sanitize_rgb_color(mixed $input):string {
if (empty($output = \filter_var($input, FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH))) {
throw new MDInvalidColorCode("Invalid color code provided: " . $output);
}
$output = \strtoupper($output);
if (!in_array(strlen($output), [3, 6], true)
|| (MD_STD::preg_replace_str('/[A-F0-9]/', '', $output) !== '')
) {
throw new MDInvalidColorCode("Invalid color code provided: " . $output);
}
return $output;
}
/**
* Valiates a list of entries, ensuring all returned values are valid IDs.
*
* @param array<mixed> $inputs Input array.
*
* @return list<positive-int>
*/
public static function sanitize_id_array(array $inputs):array {
$output = [];
foreach ($inputs as $input) {
$output[] = self::sanitize_id($input);
}
return $output;
}
/**
* Retrieves HTTP input texts from GET or POST variables, whatever is provided.
* If neither is given, returns a provided default.
*
* @param non-empty-string $var_name Variable name.
* @param string $default Default value for the output.
* @param array<string> $allowed List of allowed values. Defaults to empty (all values allowed).
*
* @return string
*/
public static function get_http_input_text(string $var_name, string $default = "", array $allowed = []):string {
if (isset($_GET[$var_name])) {
$output = self::sanitize_text($_GET[$var_name]);
}
else if (isset($_POST[$var_name])) {
$output = self::sanitize_text($_POST[$var_name]);
}
else {
$output = self::sanitize_text($default);
}
if (!empty($allowed) and !\in_array($output, $allowed, true)) {
throw new MDpageParameterNotFromListException("Parameter `{$var_name}` must be any of the allowed values: '" . implode('\', \'', $allowed) . "'");
}
return $output;
}
/**
* Retrieves HTTP input texts from POST variables.
* If none is given, returns a provided default.
*
* @param non-empty-string $var_name Variable name.
* @param string $default Default value for the output.
* @param array<string> $allowed List of allowed values. Defaults to empty (all values allowed).
*
* @return string
*/
public static function get_http_post_text(string $var_name, string $default = "", array $allowed = []):string {
if (isset($_POST[$var_name])) {
$output = self::sanitize_text($_POST[$var_name]);
}
else {
$output = self::sanitize_text($default);
}
if (!empty($allowed) and !\in_array($output, $allowed, true)) {
throw new MDpageParameterNotFromListException("Parameter `{$var_name}` must be any of the allowed values: '" . implode('\', \'', $allowed) . "'");
}
return $output;
}
/**
* Sanitizes and validates a URL. An empty string passes.
*
* @param mixed $input Input string.
*
* @return string
*/
public static function sanitize_url(mixed $input):string {
if ($input === "") {
return "";
}
try {
if (($output = \filter_var($input, FILTER_VALIDATE_URL)) === false) {
throw new MDInvalidUrl("Invalid input URL");
}
}
catch (MDInvalidUrl $e) {
if (($parsed = parse_url($input)) === false || empty($parsed['scheme']) || empty($parsed['host']) || empty($parsed['path'])) {
throw new MDInvalidUrl("Invalid input URL");
}
$rewritten = $parsed['scheme'] . '://';
if (!empty($parsed['user']) && !empty($parsed['pass'])) {
$rewritten .= $parsed['user'] . ':' . $parsed['pass'] . '@';
}
$rewritten .= $parsed['host'];
if (!empty($parsed['port'])) $rewritten .= ':' . $parsed['port'];
$rewritten .= str_replace('%2F' , '/', urlencode($parsed['path']));
if (!empty($parsed['query'])) {
$rewritten .= '?' . str_replace('%3D', '=', urlencode($parsed['query']));
}
if (($output = \filter_var($rewritten, FILTER_VALIDATE_URL)) === false) {
throw new MDInvalidUrl("Invalid input URL" . \urlencode($input));
}
}
if (empty($output)) return '';
// As per the RFC, URLs should not exceed 2048. Enough real-world ones
// do. But they certainly should not exceed 10000 characters.
if (\strlen($output) > 10000) {
throw new MDInvalidUrl("The entered URL seems to be valid otherwise, but is overly long.");
}
// Check for valid schemes
try {
if (MD_STD::startsWithAny($output, ['https://', 'http://', 'ftp://']) === false) {
throw new MDInvalidUrl("Invalid input URL" . PHP_EOL . $output . PHP_EOL . strtolower($output));
}
}
catch (MDInvalidUrl $e) {
if (MD_STD::startsWithAny(strtolower($output), ['https://', 'http://', 'ftp://']) === true) {
$output = strtolower(substr($output, 0, 8)) . substr($output, 8);
}
else throw $e;
}
if (\str_contains($output, '.') === false) {
throw new MDInvalidUrl("Invalid input URL");
}
return $output;
}
/**
* Sanitizes and validates an e-mail address. An empty string passes.
*
* @param mixed $input Input string.
*
* @return string
*/
public static function sanitize_email(mixed $input):string {
if ($input === "") {
return "";
}
if (($output = \filter_var($input, FILTER_VALIDATE_EMAIL)) === false) {
throw new MDInvalidEmail("Invalid input email address" . ' '. $input);
}
return $output;
}
/**
* Validates a password (minimum requirements: 8 characters, including
* one number and one special char) and returns a list of errors,
* if there are any.
*
* @param string $input Input string.
*
* @return array<string>
*/
public static function validate_password(string $input):array {
$errors = [];
if (mb_strlen($input) < 8) {
$errors[] = 'password_too_short';
}
if (!(\preg_match('@[0-9]@', $input)) && !(\preg_match('@[^\w]@', $input))) {
$errors[] = 'password_has_no_number_no_special_char';
}
return $errors;
}
/**
* Sanitizes and validates a phone number. An empty string passes.
*
* @param mixed $input Input string.
*
* @return string
*/
public static function validate_phone_number(mixed $input):string {
if ($input === "") {
return "";
}
if (!preg_match("#^[()0-9/ +-]+$#", $input)) {
throw new MDgenericInvalidInputsException("Invalid phone number entered.");
}
return $input;
}
/**
* Sanitizes a string to a float.
*
* @param string $input Input string.
*
* @return float
*/
public static function sanitize_float(string $input):float {
$output = \str_replace(",", ".", $input);
if (($output = \filter_var($output, FILTER_VALIDATE_FLOAT)) === false) {
throw new MDgenericInvalidInputsException("Input is readable as a floating point value");
}
return $output;
}
/**
* Validates a coordinate.
*
* @param string|integer $input Input string.
*
* @return float
*/
public static function validate_longitude(string|int|float $input):float {
if (is_string($input)) $output = self::sanitize_float($input);
else $output = $input;
if ($output < -180 || $output > 180) {
throw new MDCoordinateOutOfRange("Longitude out of range");
}
return $output;
}
/**
* Validates a coordinate.
*
* @param string|integer $input Input string.
*
* @return float
*/
public static function validate_latitude(string|int|float $input):float {
if (is_string($input)) $output = self::sanitize_float($input);
else $output = $input;
if ($output < -90 || $output > 90) {
throw new MDCoordinateOutOfRange("Latitude out of range");
}
return $output;
}
/**
* Validates ISBNs. Empty strings are accepted as well.
*
* @param string $input Input string.
*
* @return string
*/
public static function validate_isbn(string $input):string {
if ($input === "") {
return "";
}
// Remove hyphens
$input = trim(strtr($input, ["-" => "", "" => ""]));
// ISBN 10
if (\mb_strlen($input) === 10) {
if (\preg_match('/\d{9}[0-9xX]/i', $input)) {
return $input;
}
}
// ISBN 13
if (\mb_strlen($input) === 13) {
if (\preg_match('/\d{13}/i', $input)) {
return $input;
}
}
throw new MDgenericInvalidInputsException("ISBNs must be either 10 or 13 characters long.");
}
/**
* Validates a ZIP code.
*
* @param string $input Input string.
*
* @return string
*/
public static function validate_zip_code(string $input):string {
if (($input = trim($input)) === "") {
return "";
}
if (\mb_strlen($input) > 7) {
throw new MDgenericInvalidInputsException("ZIP code is too long");
}
return $input;
}
/**
* Returns an UTF8 version of a string.
*
* @param string $input Input string.
*
* @return string
*/
public static function ensureStringIsUtf8(string $input):string {
// If the input is valid UTF8 from the start, it is simply returned in its
// original form.
if (\mb_check_encoding($input, 'UTF-8')) {
return $input;
}
// To detect and convert the encoding for non-UTF8 strings, the list of
// encodings known to PHP's mbstring functions is checked against the input string.
// If any encoding matches the string, it will be converted to UTF8 accordingly.
$suitableEncodings = [];
$encodings = \mb_list_encodings();
foreach ($encodings as $encoding) {
if (\mb_detect_encoding($input, $encoding, true) !== false) {
$suitableEncodings[] = $encoding;
}
}
// If ISO-8859-1 is in the list of suitable encodings, try to convert with that.
if (\in_array('ISO-8859-1', $suitableEncodings, true)) {
if (($converted = \iconv('ISO-8859-1', "UTF-8//TRANSLIT", $input)) !== false) {
return $converted;
}
}
// If a conversion from ISO-8859-1 doesn't work, just take any of the other ones.
$suitableEncodings = \array_reverse($suitableEncodings);
foreach ($suitableEncodings as $encoding) {
if (($converted = \iconv($encoding, "UTF-8//TRANSLIT", $input)) !== false) {
return $converted;
}
}
/*
if (count($suitableEncodings) === 1) {
return mb_convert_encoding($input, 'UTF-8', );
}
*/
return $input;
}
/**
* Wrapper around move_uploaded_file that throws errors in case the upload failed
* for an identifiable reason.
*
* @param non-empty-string $filename Name of the file to upload.
* @param non-empty-string $destination Destination to move the file to.
* @param array<string> $mime_types Optional array of acceptable mime types. If this is
* not empty, the file will be checked for having one
* of the given mime types. If it does not, an error
* will be thrown.
*
* @return boolean
*/
public static function move_uploaded_file(string $filename, string $destination, array $mime_types = []):bool {
MD_STD::ensure_file($filename, $mime_types);
if (empty($destDir = dirname($destination))) {
return false;
}
MD_STD::check_is_writable($destDir);
if (!(\move_uploaded_file($filename, $destination))) {
return false;
}
return true;
}
}