$rules) { // Loop through each validation rule for the set item foreach ($rules as $rule => $rule_value) { $value = trim($source[$item]); // Escape the item's contents just in case $item = Output::getClean($item); // Required rule if ($rule === self::REQUIRED) { $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') { $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.", ]); continue; } } if (empty($value) && $value !== '0') { continue; } // The post array does include this value, continue validating switch ($rule) { case self::MIN: if (mb_strlen($value) < $rule_value) { $validator->addError([ 'field' => $item, 'rule' => self::MIN, 'fallback' => "$item must be a minimum of $rule_value characters.", ]); } break; case self::MAX: if (mb_strlen($value) > $rule_value) { $validator->addError([ 'field' => $item, 'rule' => self::MAX, 'fallback' => "$item must be a maximum of $rule_value characters.", ]); } break; case self::MATCHES: if ($value != $source[$rule_value]) { $validator->addError([ 'field' => $item, 'rule' => self::MATCHES, 'fallback' => "$rule_value must match $item.", ]); } break; case self::AGREE: if ($value != 1) { $validator->addError([ 'field' => $item, 'rule' => self::AGREE, 'fallback' => 'You must agree to our terms and conditions in order to register.', ]); } 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; case self::UNIQUE: if (is_array($rule_value)) { $table = $rule_value[0]; [$ignore_col, $ignore_val] = explode(':', $rule_value[1]); $sql = << ? SQL; $check = DB::getInstance()->query($sql, [$value, $ignore_val]); } else { $table = $rule_value; $check = DB::getInstance()->get($table, [$item, $value]); } if ($check->count()) { $validator->addError([ 'field' => $item, 'rule' => self::UNIQUE, 'fallback' => "The $rule_value.$item $value already exists!", ]); } break; case self::EMAIL: if (!filter_var($value, FILTER_VALIDATE_EMAIL)) { $validator->addError([ 'field' => $item, 'rule' => self::EMAIL, 'fallback' => "$value is not a valid email.", ]); } break; case self::TIMEZONE: if (!in_array($value, DateTimeZone::listIdentifiers())) { $validator->addError([ 'field' => $item, 'rule' => self::TIMEZONE, 'fallback' => "The timezone $value is invalid.", ]); } break; case self::IS_ACTIVE: $check = DB::getInstance()->query('SELECT * FROM nl2_users WHERE username = ? OR email = ?', [$value, $value]); if (!$check->count()) { break; } $isuseractive = $check->first()->active; if ($isuseractive == 0) { $validator->addError([ 'field' => $item, 'rule' => self::IS_ACTIVE, 'fallback' => "That $item is inactive. Have you validated your account or requested a password reset?", ]); } break; case self::IS_BANNED: $check = DB::getInstance()->get('users', [$item, $value]); if (!$check->count()) { break; } $isuserbanned = $check->first()->isbanned; if ($isuserbanned == 1) { $validator->addError([ 'field' => $item, 'rule' => self::IS_BANNED, 'fallback' => "The username $value is banned.", ]); } break; case self::ALPHANUMERIC: if (!ctype_alnum($value)) { $validator->addError([ 'field' => $item, 'rule' => self::ALPHANUMERIC, 'fallback' => "$item must be alphanumeric.", ]); } break; case self::NUMERIC: if (!is_numeric($value)) { $validator->addError([ 'field' => $item, 'rule' => self::NUMERIC, 'fallback' => "$item must be numeric.", ]); } break; 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.", ]); } 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; } } } if (empty($validator->_to_convert)) { // Only return true if there are no errors $validator->_passed = true; } return $validator; } /** * 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 { $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, ]; } /** * Add generic message for any failures, specific `messages()` will override this. * * @param string $message message to show if any failures occur. * * @return Validate This instance of Validate. */ public function message(string $message): Validate { $this->_message = $message; return $this; } /** * Add custom messages to this `Validate` instance. * * @param array $messages array of input names and strings or arrays to use as messages. * * @return Validate This instance of Validate. */ public function messages(array $messages): Validate { $this->_messages = $messages; return $this; } /** * Translate temp error information to their specific or generic or fallback messages and return. * * @return array Any and all errors for this `Validate` instance. */ public function errors(): array { // 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']); // If there is no generic `message()` set or the translated message is not equal to generic message // we can continue without worrying about duplications if ($this->_message === null || ($message != $this->_message && !in_array($message, $this->_errors))) { $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; } /** * 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. * * @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. * * @return string Message for this field and rule. */ private function getMessage(string $field, string $rule, string $fallback, ?array $meta = []): string { // 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; } // 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; } // If the message is a callback function, provide it with meta if (is_callable($this->_messages[$field][$rule])) { return $this->_messages[$field][$rule]($meta); } // Rule-specific custom message was supplied return $this->_messages[$field][$rule]; } /** * Get if this `Validate` instance passed. * * @return bool whether this 'Validate' passed or not. */ public function passed(): bool { return $this->_passed; } }