mc-cms-namelessmc/core/classes/Core/Validate.php

619 lines
23 KiB
PHP
Raw Permalink Normal View History

<?php
/**
* Validates an array of data.
* Often used for POST requests.
*
* @package NamelessMC\Core
* @author Samerton
* @author Aberdeener
* @version 2.2.0
* @license MIT
*/
class Validate
{
2021-04-06 19:21:49 -07:00
/**
* @var string Ensure this field is not empty
2021-04-06 19:21:49 -07:00
*/
2022-01-15 15:18:05 -08:00
public const REQUIRED = 'required';
2021-04-06 19:21:49 -07:00
/**
* @var string Define minimum number of characters
2021-04-06 19:21:49 -07:00
*/
2022-01-15 15:18:05 -08:00
public const MIN = 'min';
2021-04-06 19:21:49 -07:00
/**
* @var string Define max number of characters
2021-04-06 19:21:49 -07:00
*/
2022-01-15 15:18:05 -08:00
public const MAX = 'max';
2021-04-06 19:21:49 -07:00
/**
* @var string Ensure provided value matches another
2021-04-06 19:21:49 -07:00
*/
2022-01-15 15:18:05 -08:00
public const MATCHES = 'matches';
2021-04-06 19:21:49 -07:00
/**
* @var string Check the user has agreed to the terms and conditions
2021-04-06 19:21:49 -07:00
*/
2022-01-15 15:18:05 -08:00
public const AGREE = 'agree';
/**
* @var string Check the numeric value is at least x
*/
public const AT_LEAST = 'at_least';
/**
* @var string Check the numeric value is at most x
*/
public const AT_MOST = 'at_most';
2021-04-06 19:21:49 -07:00
/**
* @var string Check the value has not already been inputted in the database
2021-04-06 19:21:49 -07:00
*/
2022-01-15 15:18:05 -08:00
public const UNIQUE = 'unique';
2021-04-06 19:21:49 -07:00
/**
* @var string Check if email is valid
2021-04-06 19:21:49 -07:00
*/
2022-01-15 15:18:05 -08:00
public const EMAIL = 'email';
2021-04-06 19:21:49 -07:00
/**
* @var string Check that timezone is valid
2021-04-06 19:21:49 -07:00
*/
2022-01-15 15:18:05 -08:00
public const TIMEZONE = 'timezone';
2021-04-06 19:21:49 -07:00
/**
* @var string Check that the specified user account is set as active (ie validated)
2021-04-06 19:21:49 -07:00
*/
2022-01-15 15:18:05 -08:00
public const IS_ACTIVE = 'isactive';
2021-04-06 19:21:49 -07:00
/**
* @var string Check that the specified user account is not banned
2021-04-06 19:21:49 -07:00
*/
2022-01-15 15:18:05 -08:00
public const IS_BANNED = 'isbanned';
2021-04-06 19:21:49 -07:00
/**
* @var string Check that the value is alphanumeric
2021-04-06 19:21:49 -07:00
*/
2022-01-15 15:18:05 -08:00
public const ALPHANUMERIC = 'alphanumeric';
2021-04-06 19:21:49 -07:00
/**
* @var string Check that the value is numeric
2021-04-06 19:21:49 -07:00
*/
2022-01-15 15:18:05 -08:00
public const NUMERIC = 'numeric';
/**
* @var string Check that the value is in of a set of values
*/
public const IN = 'in';
2022-04-07 11:48:45 -07:00
/**
* @var string Check that the value matches a regex pattern
*/
public const REGEX = 'regex';
/**
* @var string Check that the value does not start with a pattern
*/
public const NOT_START_WITH = 'not_start_with';
/**
* @var string Check that the value does not contain a pattern
*/
public const NOT_CONTAIN = 'not_contain';
/**
* @var string Set a rate limit
*/
public const RATE_LIMIT = 'rate_limit';
/**
* @var string Custom validation check
*/
public const CUSTOM = 'custom';
2021-12-07 22:14:12 -08:00
private ?string $_message = null;
private array $_messages = [];
private bool $_passed = false;
private array $_to_convert = [];
private array $_errors = [];
2021-04-06 19:21:49 -07:00
/**
* Validate an array of inputs.
2021-12-07 22:14:12 -08:00
*
2021-04-06 19:21:49 -07:00
* @param array $source inputs (eg: $_POST)
* @param array $items subset of inputs to be validated
2021-12-07 22:14:12 -08:00
*
* @throws Exception If provided configuration for a rule is invalid - not if a provided value is invalid!
* @return Validate New instance of Validate.
2021-04-06 19:21:49 -07:00
*/
public static function check(array $source, array $items = []): Validate
{
$validator = new Validate();
2020-12-13 19:38:04 +01:00
// Loop through the items which need validating
2020-12-13 20:42:04 -08:00
foreach ($items as $item => $rules) {
2020-12-13 19:38:04 +01:00
// Loop through each validation rule for the set item
2020-12-13 20:42:04 -08:00
foreach ($rules as $rule => $rule_value) {
2020-12-13 19:38:04 +01:00
$value = trim($source[$item]);
// Escape the item's contents just in case
$item = Output::getClean($item);
// Required rule
if ($rule === self::REQUIRED) {
2022-04-18 10:33:18 -07:00
$missing = false;
// If the item is HTML array syntax, check if it exists within the subarray.
// Otherwise, check if it's empty.
if (str_contains($item, '[') && str_ends_with($item, ']')) {
preg_match('/\[(.*?)\]/', $item, $matches);
$array = explode('[', $item)[0];
if (empty($source[$array][$matches[1]])) {
$missing = true;
}
} elseif (empty($value) && $value !== '0') {
2022-04-18 10:33:18 -07:00
$missing = true;
}
if ($missing) {
// The post array does not include this value, return an error
$validator->addError([
'field' => $item,
'rule' => self::REQUIRED,
'fallback' => "$item is required.",
2022-04-18 10:33:18 -07:00
]);
continue;
}
2021-12-07 22:14:12 -08:00
}
if (empty($value) && $value !== '0') {
2021-04-06 19:21:49 -07:00
continue;
}
// The post array does include this value, continue validating
switch ($rule) {
2022-01-15 15:18:05 -08:00
case self::MIN:
2021-04-06 19:21:49 -07:00
if (mb_strlen($value) < $rule_value) {
$validator->addError([
2021-04-06 19:21:49 -07:00
'field' => $item,
2022-01-15 15:18:05 -08:00
'rule' => self::MIN,
'fallback' => "$item must be a minimum of $rule_value characters.",
2021-04-06 19:21:49 -07:00
]);
}
break;
2022-01-15 15:18:05 -08:00
case self::MAX:
2021-04-06 19:21:49 -07:00
if (mb_strlen($value) > $rule_value) {
$validator->addError([
2021-04-06 19:21:49 -07:00
'field' => $item,
2022-01-15 15:18:05 -08:00
'rule' => self::MAX,
'fallback' => "$item must be a maximum of $rule_value characters.",
2021-04-06 19:21:49 -07:00
]);
}
break;
2022-01-15 15:18:05 -08:00
case self::MATCHES:
2021-04-06 19:21:49 -07:00
if ($value != $source[$rule_value]) {
$validator->addError([
2021-04-06 19:21:49 -07:00
'field' => $item,
2022-01-15 15:18:05 -08:00
'rule' => self::MATCHES,
'fallback' => "$rule_value must match $item.",
2021-04-06 19:21:49 -07:00
]);
}
break;
2022-01-15 15:18:05 -08:00
case self::AGREE:
2021-04-06 19:21:49 -07:00
if ($value != 1) {
$validator->addError([
2021-04-06 19:21:49 -07:00
'field' => $item,
2022-01-15 15:18:05 -08:00
'rule' => self::AGREE,
'fallback' => 'You must agree to our terms and conditions in order to register.',
2021-04-06 19:21:49 -07:00
]);
}
break;
case self::AT_LEAST:
if (floatval($value) < $rule_value) {
$validator->addError([
'field' => $item,
'rule' => self::AT_LEAST,
'fallback' => "$item must have a value of at least $rule_value.",
'meta' => ['min' => $rule_value],
]);
}
break;
case self::AT_MOST:
if (floatval($value) > $rule_value) {
$validator->addError([
'field' => $item,
'rule' => self::AT_MOST,
'fallback' => "$item must have a value of at most $rule_value.",
'meta' => ['max' => $rule_value],
]);
}
break;
2022-01-15 15:18:05 -08:00
case self::UNIQUE:
if (is_array($rule_value)) {
$table = $rule_value[0];
[$ignore_col, $ignore_val] = explode(':', $rule_value[1]);
2023-01-14 14:28:54 +00:00
$sql =
<<<SQL
SELECT *
FROM nl2_$table
WHERE $item = ?
AND $ignore_col <> ?
SQL;
$check = DB::getInstance()->query($sql, [$value, $ignore_val]);
} else {
$table = $rule_value;
$check = DB::getInstance()->get($table, [$item, $value]);
}
2021-04-06 19:21:49 -07:00
if ($check->count()) {
$validator->addError([
2021-04-06 19:21:49 -07:00
'field' => $item,
2022-01-15 15:18:05 -08:00
'rule' => self::UNIQUE,
'fallback' => "The $rule_value.$item $value already exists!",
2021-04-06 19:21:49 -07:00
]);
}
break;
2022-01-15 15:18:05 -08:00
case self::EMAIL:
2021-04-06 19:21:49 -07:00
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
$validator->addError([
2021-04-06 19:21:49 -07:00
'field' => $item,
2022-01-15 15:18:05 -08:00
'rule' => self::EMAIL,
'fallback' => "$value is not a valid email.",
2021-04-06 19:21:49 -07:00
]);
}
break;
2022-01-15 15:18:05 -08:00
case self::TIMEZONE:
2021-04-06 19:21:49 -07:00
if (!in_array($value, DateTimeZone::listIdentifiers())) {
$validator->addError([
2021-04-06 19:21:49 -07:00
'field' => $item,
2022-01-15 15:18:05 -08:00
'rule' => self::TIMEZONE,
'fallback' => "The timezone $value is invalid.",
2021-04-06 19:21:49 -07:00
]);
}
break;
2022-01-15 15:18:05 -08:00
case self::IS_ACTIVE:
$check = DB::getInstance()->query('SELECT * FROM nl2_users WHERE username = ? OR email = ?', [$value, $value]);
2021-04-06 19:21:49 -07:00
if (!$check->count()) {
break;
2021-04-06 19:21:49 -07:00
}
$isuseractive = $check->first()->active;
if ($isuseractive == 0) {
$validator->addError([
2021-04-06 19:21:49 -07:00
'field' => $item,
2022-01-15 15:18:05 -08:00
'rule' => self::IS_ACTIVE,
'fallback' => "That $item is inactive. Have you validated your account or requested a password reset?",
2021-04-06 19:21:49 -07:00
]);
}
break;
2022-01-15 15:18:05 -08:00
case self::IS_BANNED:
$check = DB::getInstance()->get('users', [$item, $value]);
2021-04-06 19:21:49 -07:00
if (!$check->count()) {
break;
2021-04-06 19:21:49 -07:00
}
$isuserbanned = $check->first()->isbanned;
if ($isuserbanned == 1) {
$validator->addError([
2021-04-06 19:21:49 -07:00
'field' => $item,
2022-01-15 15:18:05 -08:00
'rule' => self::IS_BANNED,
'fallback' => "The username $value is banned.",
2021-04-06 19:21:49 -07:00
]);
}
break;
2022-01-15 15:18:05 -08:00
case self::ALPHANUMERIC:
2021-04-06 19:21:49 -07:00
if (!ctype_alnum($value)) {
$validator->addError([
2021-04-06 19:21:49 -07:00
'field' => $item,
2022-01-15 15:18:05 -08:00
'rule' => self::ALPHANUMERIC,
'fallback' => "$item must be alphanumeric.",
2021-04-06 19:21:49 -07:00
]);
}
break;
2022-01-15 15:18:05 -08:00
case self::NUMERIC:
2021-04-06 19:21:49 -07:00
if (!is_numeric($value)) {
$validator->addError([
2021-04-06 19:21:49 -07:00
'field' => $item,
2022-01-15 15:18:05 -08:00
'rule' => self::NUMERIC,
'fallback' => "$item must be numeric.",
2021-04-06 19:21:49 -07:00
]);
}
2020-12-13 19:38:04 +01:00
break;
2022-04-07 11:48:45 -07:00
case self::REGEX:
if (!preg_match($rule_value, $value)) {
$validator->addError([
'field' => $item,
'rule' => self::REGEX,
'fallback' => "$item does not match the pattern $rule_value.",
2022-04-07 11:48:45 -07:00
]);
}
break;
case self::NOT_START_WITH:
$denied_values = is_string($rule_value) ? [$rule_value] : $rule_value;
foreach ($denied_values as $denied_value) {
if (str_starts_with($value, $denied_value)) {
$validator->addError([
'field' => $item,
'rule' => self::NOT_START_WITH,
'fallback' => "$item must not start with $denied_value.",
]);
break;
}
}
break;
case self::NOT_CONTAIN:
if (!is_array($rule_value)) {
$rule_value = [$rule_value];
}
foreach ($rule_value as $term) {
if (strpos(strtolower($value), strtolower(trim($term))) !== false) {
$validator->addError([
'field' => $item,
'rule' => self::NOT_CONTAIN,
'fallback' => "$item must not contain $term",
]);
break;
}
}
break;
case self::IN:
$values = is_string($rule_value) ? [$rule_value] : $rule_value;
if (!in_array($value, $values)) {
$string_values = implode(', ', $values);
$validator->addError([
'field' => $item,
'rule' => self::IN,
'fallback' => "$item must be one of $string_values.",
]);
}
break;
case self::RATE_LIMIT:
if (is_array($rule_value) && count($rule_value) === 2) {
// If array treat as [limit, seconds]
[$limit, $seconds] = $rule_value;
} elseif (is_int($rule_value)) {
// If integer default seconds to 60
[$limit, $seconds] = [$rule_value, 60];
}
if (!isset($limit) || !isset($seconds)) {
throw new Exception('Invalid rate limit configuration');
}
$key = "rate_limit_{$item}";
$session = $_SESSION[$key];
$time = date('U');
$limit_end = $time + $seconds;
if (isset($session) && is_array($session) && count($session) === 2) {
[$count, $expires] = $session;
$diff = $expires - $time;
if (++$count >= $limit && $diff > 0) {
$validator->addError([
'field' => $item,
'rule' => self::RATE_LIMIT,
'fallback' => "$item has reached the rate limit which expires in $diff seconds.",
'meta' => ['expires' => $diff],
]);
break;
}
if ($diff <= 0) {
// Reset
$_SESSION[$key] = [1, $limit_end];
break;
}
$_SESSION[$key] = [$count, $expires];
} else {
$_SESSION[$key] = [1, $limit_end];
}
break;
case self::CUSTOM:
if (!$rule_value instanceof Closure) {
throw new Exception('Custom rule must be a instance of closure.');
}
$reflection = new ReflectionFunction($rule_value);
$reflectionParams = $reflection->getParameters();
if (count($reflectionParams) !== 2) {
throw new InvalidArgumentException('Custom rule closure must take 2 arguments (Validate and the field item).');
}
// if they've provided a typehint for the first argument, make sure it's taking Validate
$param = $reflectionParams[0];
if ($param->getType() instanceof ReflectionNamedType && $param->getType()->getName() !== Validate::class) {
throw new InvalidArgumentException('Custom rule closure must take Validate as the first argument.');
}
// check that the second argument is a string
$param = $reflectionParams[1];
if ($param->getType() instanceof ReflectionNamedType && $param->getType()->getName() !== 'string') {
throw new InvalidArgumentException('Custom rule closure must take a string as the second argument.');
}
$rule_value($validator, $item);
break;
2020-12-13 19:38:04 +01:00
}
}
}
if (empty($validator->_to_convert)) {
2020-12-13 19:38:04 +01:00
// Only return true if there are no errors
$validator->_passed = true;
2020-12-13 19:38:04 +01:00
}
return $validator;
2020-12-13 19:38:04 +01:00
}
2021-12-07 22:14:12 -08:00
/**
* Add an array of information to generate an error message to the $_to_convert array.
* These errors will be translated in the `errors()` function later.
*
* @param array $error message to add to error array
*/
private function addError(array $error): void
{
2021-12-07 22:14:12 -08:00
$this->_to_convert[] = $error;
}
/**
* Add an array of information to generate an error message to the $_to_convert array.
* These errors will be translated in the `errors()` function later.
*
* @param string $item field item
* @param string $error error message
* @param array $meta error metadata
*/
public function addCustomError(string $item, string $error, array $meta = []): void
{
$this->_to_convert[] = [
'field' => $item,
'rule' => self::CUSTOM,
'fallback' => $error,
'meta' => $meta,
];
}
2021-04-06 19:21:49 -07:00
/**
* Add generic message for any failures, specific `messages()` will override this.
2021-12-07 22:14:12 -08:00
*
* @param string $message message to show if any failures occur.
2021-12-07 22:14:12 -08:00
*
* @return Validate This instance of Validate.
2021-04-06 19:21:49 -07:00
*/
public function message(string $message): Validate
{
2021-04-06 19:21:49 -07:00
$this->_message = $message;
2021-04-06 19:21:49 -07:00
return $this;
}
/**
* Add custom messages to this `Validate` instance.
2021-12-07 22:14:12 -08:00
*
* @param array $messages array of input names and strings or arrays to use as messages.
2021-12-07 22:14:12 -08:00
*
* @return Validate This instance of Validate.
2021-04-06 19:21:49 -07:00
*/
public function messages(array $messages): Validate
{
2021-04-06 19:21:49 -07:00
$this->_messages = $messages;
2021-04-06 19:21:49 -07:00
return $this;
}
/**
2021-12-07 22:14:12 -08:00
* Translate temp error information to their specific or generic or fallback messages and return.
*
* @return array Any and all errors for this `Validate` instance.
2021-04-06 19:21:49 -07:00
*/
public function errors(): array
{
2021-12-07 22:14:12 -08:00
// If errors have already been translated, don't waste time redoing it
if (!empty($this->_errors)) {
return $this->_errors;
}
// Loop all errors to convert and get their custom messages
foreach ($this->_to_convert as $error) {
$message = $this->getMessage($error['field'], $error['rule'], $error['fallback'], $error['meta']);
2021-12-07 22:14:12 -08:00
// If there is no generic `message()` set or the translated message is not equal to generic message
// we can continue without worrying about duplications
2022-07-03 20:56:15 +02:00
if ($this->_message === null || ($message != $this->_message && !in_array($message, $this->_errors))) {
2021-12-07 22:14:12 -08:00
$this->_errors[] = $message;
continue;
}
// If this new error is the generic message AND it has not already been added, add it
if ($message == $this->_message && !in_array($this->_message, $this->_errors)) {
$this->_errors[] = $this->_message;
}
}
return $this->_errors;
2021-04-06 19:21:49 -07:00
}
/**
* Get the error message for a field.
* Priority:
* - Message is set for the field and rule
* - Message for field, not rule specific
* - Result of callable if "*" rule exists
* - Generic message set with `message(...)`
* - Fallback message for rule.
2021-12-07 22:14:12 -08:00
*
* @param string $field name of field to search for.
* @param string $rule rule which check failed. should be from the constants defined above.
* @param string $fallback fallback default message if custom message and generic message are not supplied.
* @param ?array $meta optional meta to provide to message.
2021-12-07 22:14:12 -08:00
*
2021-09-21 19:21:14 -07:00
* @return string Message for this field and rule.
2021-04-06 19:21:49 -07:00
*/
private function getMessage(string $field, string $rule, string $fallback, ?array $meta = []): string
{
2021-04-06 19:21:49 -07:00
// No custom messages defined for this field
if (!isset($this->_messages[$field])) {
if (isset($this->_messages['*'])) {
$message = $this->_messages['*']($field);
if ($message !== null) {
return $message;
}
}
return $this->_message ?? $fallback;
2021-04-06 19:21:49 -07:00
}
// Generic custom message for this field supplied - but not rule specific
if (!is_array($this->_messages[$field])) {
return $this->_messages[$field];
}
// Array of custom messages supplied, but none of their rules matches this rule
if (!array_key_exists($rule, $this->_messages[$field])) {
return $this->_message ?? $fallback;
2021-04-06 19:21:49 -07:00
}
// If the message is a callback function, provide it with meta
if (is_callable($this->_messages[$field][$rule])) {
return $this->_messages[$field][$rule]($meta);
}
2021-04-06 19:21:49 -07:00
// Rule-specific custom message was supplied
return $this->_messages[$field][$rule];
2020-12-13 19:38:04 +01:00
}
2021-04-06 19:21:49 -07:00
/**
* Get if this `Validate` instance passed.
2021-12-07 22:14:12 -08:00
*
2021-10-11 21:31:23 +02:00
* @return bool whether this 'Validate' passed or not.
2021-04-06 19:21:49 -07:00
*/
public function passed(): bool
{
2020-12-13 19:38:04 +01:00
return $this->_passed;
}
2017-02-21 13:36:45 +01:00
}