add trashes

This commit is contained in:
Diskette Guy
2025-07-21 15:40:51 +07:00
parent c8f2944770
commit 07171f5b5a
848 changed files with 134166 additions and 0 deletions
@@ -0,0 +1,283 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Validation;
/**
* Class CreditCardRules
*
* Provides validation methods for common credit-card inputs.
*
* @see http://en.wikipedia.org/wiki/Credit_card_number
* @see \CodeIgniter\Validation\CreditCardRulesTest
*/
class CreditCardRules
{
/**
* The cards that we support, with the defining details:
*
* name - The type of card as found in the form. Must match the user's value
* length - List of possible lengths for the card number
* prefixes - List of possible prefixes for the card
* checkdigit - Boolean on whether we should do a modulus10 check on the numbers.
*
* @var array
*/
protected $cards = [
'American Express' => [
'name' => 'amex',
'length' => '15',
'prefixes' => '34,37',
'checkdigit' => true,
],
'China UnionPay' => [
'name' => 'unionpay',
'length' => '16,17,18,19',
'prefixes' => '62',
'checkdigit' => true,
],
'Dankort' => [
'name' => 'dankort',
'length' => '16',
'prefixes' => '5019,4175,4571,4',
'checkdigit' => true,
],
'DinersClub' => [
'name' => 'dinersclub',
'length' => '14,16',
'prefixes' => '300,301,302,303,304,305,309,36,38,39,54,55',
'checkdigit' => true,
],
'DinersClub CarteBlanche' => [
'name' => 'carteblanche',
'length' => '14',
'prefixes' => '300,301,302,303,304,305',
'checkdigit' => true,
],
'Discover Card' => [
'name' => 'discover',
'length' => '16,19',
'prefixes' => '6011,622,644,645,656,647,648,649,65',
'checkdigit' => true,
],
'InterPayment' => [
'name' => 'interpayment',
'length' => '16,17,18,19',
'prefixes' => '4',
'checkdigit' => true,
],
'JCB' => [
'name' => 'jcb',
'length' => '16,17,18,19',
'prefixes' => '352,353,354,355,356,357,358',
'checkdigit' => true,
],
'Maestro' => [
'name' => 'maestro',
'length' => '12,13,14,15,16,18,19',
'prefixes' => '50,56,57,58,59,60,61,62,63,64,65,66,67,68,69',
'checkdigit' => true,
],
'MasterCard' => [
'name' => 'mastercard',
'length' => '16',
'prefixes' => '51,52,53,54,55,22,23,24,25,26,27',
'checkdigit' => true,
],
'NSPK MIR' => [
'name' => 'mir',
'length' => '16',
'prefixes' => '2200,2201,2202,2203,2204',
'checkdigit' => true,
],
'Troy' => [
'name' => 'troy',
'length' => '16',
'prefixes' => '979200,979289',
'checkdigit' => true,
],
'UATP' => [
'name' => 'uatp',
'length' => '15',
'prefixes' => '1',
'checkdigit' => true,
],
'Verve' => [
'name' => 'verve',
'length' => '16,19',
'prefixes' => '506,650',
'checkdigit' => true,
],
'Visa' => [
'name' => 'visa',
'length' => '13,16,19',
'prefixes' => '4',
'checkdigit' => true,
],
// Canadian Cards
'BMO ABM Card' => [
'name' => 'bmoabm',
'length' => '16',
'prefixes' => '500',
'checkdigit' => false,
],
'CIBC Convenience Card' => [
'name' => 'cibc',
'length' => '16',
'prefixes' => '4506',
'checkdigit' => false,
],
'HSBC Canada Card' => [
'name' => 'hsbc',
'length' => '16',
'prefixes' => '56',
'checkdigit' => false,
],
'Royal Bank of Canada Client Card' => [
'name' => 'rbc',
'length' => '16',
'prefixes' => '45',
'checkdigit' => false,
],
'Scotiabank Scotia Card' => [
'name' => 'scotia',
'length' => '16',
'prefixes' => '4536',
'checkdigit' => false,
],
'TD Canada Trust Access Card' => [
'name' => 'tdtrust',
'length' => '16',
'prefixes' => '589297',
'checkdigit' => false,
],
];
/**
* Verifies that a credit card number is valid and matches the known
* formats for a wide number of credit card types. This does not verify
* that the card is a valid card, only that the number is formatted correctly.
*
* Example:
* $rules = [
* 'cc_num' => 'valid_cc_number[visa]'
* ];
*/
public function valid_cc_number(?string $ccNumber, string $type): bool
{
$type = strtolower($type);
$info = null;
// Get our card info based on provided name.
foreach ($this->cards as $card) {
if ($card['name'] === $type) {
$info = $card;
break;
}
}
// If empty, it's not a card type we recognize, or invalid type.
if ($info === null) {
return false;
}
// Make sure we have a valid length
if ((string) $ccNumber === '') {
return false;
}
// Remove any spaces and dashes
$ccNumber = str_replace([' ', '-'], '', $ccNumber);
// Non-numeric values cannot be a number...duh
if (! is_numeric($ccNumber)) {
return false;
}
// Make sure it's a valid length for this card
$lengths = explode(',', $info['length']);
if (! in_array((string) strlen($ccNumber), $lengths, true)) {
return false;
}
// Make sure it has a valid prefix
$prefixes = explode(',', $info['prefixes']);
$validPrefix = false;
foreach ($prefixes as $prefix) {
if (str_starts_with($ccNumber, $prefix)) {
$validPrefix = true;
break;
}
}
if ($validPrefix === false) {
return false;
}
// Still here? Then check the number against the Luhn algorithm, if required
// @see https://en.wikipedia.org/wiki/Luhn_algorithm
// @see https://gist.github.com/troelskn/1287893
if ($info['checkdigit'] === true) {
return $this->isValidLuhn($ccNumber);
}
return true;
}
/**
* Checks the given number to see if the number passing a Luhn check.
*/
protected function isValidLuhn(?string $number = null): bool
{
$number = (string) $number;
$sumTable = [
[
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
],
[
0,
2,
4,
6,
8,
1,
3,
5,
7,
9,
],
];
$sum = 0;
$flip = 0;
for ($i = strlen($number) - 1; $i >= 0; $i--) {
$sum += $sumTable[$flip++ & 0x1][$number[$i]];
}
return $sum % 10 === 0;
}
}
@@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Validation;
/**
* @see \CodeIgniter\Validation\DotArrayFilterTest
*/
final class DotArrayFilter
{
/**
* Creates a new array with only the elements specified in dot array syntax.
*
* @param array $indexes The dot array syntax pattern to use for filtering.
* @param array $array The array to filter.
*
* @return array The filtered array.
*/
public static function run(array $indexes, array $array): array
{
$result = [];
foreach ($indexes as $index) {
$segments = preg_split('/(?<!\\\\)\./', $index, -1, PREG_SPLIT_NO_EMPTY);
$segments = array_map(static fn ($key): string => str_replace('\.', '.', $key), $segments);
$filteredArray = self::filter($segments, $array);
if ($filteredArray !== []) {
$result = array_replace_recursive($result, $filteredArray);
}
}
return $result;
}
/**
* Used by `run()` to recursively filter the array with wildcards.
*
* @param array $indexes The dot array syntax pattern to use for filtering.
* @param array $array The array to filter.
*
* @return array The filtered array.
*/
private static function filter(array $indexes, array $array): array
{
// If there are no indexes left, return an empty array
if ($indexes === []) {
return [];
}
// Get the current index
$currentIndex = array_shift($indexes);
// If the current index doesn't exist and is not a wildcard, return an empty array
if (! isset($array[$currentIndex]) && $currentIndex !== '*') {
return [];
}
// Handle the wildcard '*' at the current level
if ($currentIndex === '*') {
$result = [];
// Iterate over all keys at this level
foreach ($array as $key => $value) {
if ($indexes === []) {
// If no indexes are left, capture the entire value
$result[$key] = $value;
} elseif (is_array($value)) {
// If there are still indexes left, continue filtering recursively
$filtered = self::filter($indexes, $value);
if ($filtered !== []) {
$result[$key] = $filtered;
}
}
}
return $result;
}
// If this is the last index, return the value
if ($indexes === []) {
return [$currentIndex => $array[$currentIndex] ?? []];
}
// If the current value is an array, recursively filter it
if (is_array($array[$currentIndex])) {
$filtered = self::filter($indexes, $array[$currentIndex]);
if ($filtered !== []) {
return [$currentIndex => $filtered];
}
}
return [];
}
}
@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Validation\Exceptions;
use CodeIgniter\Exceptions\FrameworkException;
class ValidationException extends FrameworkException
{
/**
* Throws when the validation rule is not found.
*
* @return static
*/
public static function forRuleNotFound(?string $rule = null)
{
return new static(lang('Validation.ruleNotFound', [$rule]));
}
/**
* Throws when the group value of config is not set.
*
* @return static
*/
public static function forGroupNotFound(?string $group = null)
{
return new static(lang('Validation.groupNotFound', [$group]));
}
/**
* Throws when the group value of config is not array type.
*
* @return static
*/
public static function forGroupNotArray(?string $group = null)
{
return new static(lang('Validation.groupNotArray', [$group]));
}
/**
* Throws when the template of config is invalid.
*
* @return static
*/
public static function forInvalidTemplate(?string $template = null)
{
return new static(lang('Validation.invalidTemplate', [$template]));
}
/**
* Throws when there is no any rule set.
*
* @return static
*/
public static function forNoRuleSets()
{
return new static(lang('Validation.noRuleSets'));
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Validation;
use CodeIgniter\Validation\StrictRules\FileRules as StrictFileRules;
/**
* File validation rules
*
* @see \CodeIgniter\Validation\FileRulesTest
*/
class FileRules extends StrictFileRules
{
}
@@ -0,0 +1,472 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Validation;
use DateTime;
/**
* Format validation Rules.
*
* @see \CodeIgniter\Validation\FormatRulesTest
*/
class FormatRules
{
/**
* Alpha
*
* @param string|null $str
*/
public function alpha($str = null): bool
{
if (! is_string($str)) {
$str = (string) $str;
}
return ctype_alpha($str);
}
/**
* Alpha with spaces.
*
* @param string|null $value Value.
*
* @return bool True if alpha with spaces, else false.
*/
public function alpha_space($value = null): bool
{
if ($value === null) {
return true;
}
if (! is_string($value)) {
$value = (string) $value;
}
// @see https://regex101.com/r/LhqHPO/1
return (bool) preg_match('/\A[A-Z ]+\z/i', $value);
}
/**
* Alphanumeric with underscores and dashes
*
* @see https://regex101.com/r/XfVY3d/1
*
* @param string|null $str
*/
public function alpha_dash($str = null): bool
{
if ($str === null) {
return false;
}
if (! is_string($str)) {
$str = (string) $str;
}
return preg_match('/\A[a-z0-9_-]+\z/i', $str) === 1;
}
/**
* Alphanumeric, spaces, and a limited set of punctuation characters.
* Accepted punctuation characters are: ~ tilde, ! exclamation,
* # number, $ dollar, % percent, & ampersand, * asterisk, - dash,
* _ underscore, + plus, = equals, | vertical bar, : colon, . period
* ~ ! # $ % & * - _ + = | : .
*
* @param string|null $str
*
* @return bool
*
* @see https://regex101.com/r/6N8dDY/1
*/
public function alpha_numeric_punct($str)
{
if ($str === null) {
return false;
}
if (! is_string($str)) {
$str = (string) $str;
}
return preg_match('/\A[A-Z0-9 ~!#$%\&\*\-_+=|:.]+\z/i', $str) === 1;
}
/**
* Alphanumeric
*
* @param string|null $str
*/
public function alpha_numeric($str = null): bool
{
if (! is_string($str)) {
$str = (string) $str;
}
return ctype_alnum($str);
}
/**
* Alphanumeric w/ spaces
*
* @param string|null $str
*/
public function alpha_numeric_space($str = null): bool
{
if (! is_string($str)) {
$str = (string) $str;
}
// @see https://regex101.com/r/0AZDME/1
return (bool) preg_match('/\A[A-Z0-9 ]+\z/i', $str);
}
/**
* Any type of string
*
* Note: we specifically do NOT type hint $str here so that
* it doesn't convert numbers into strings.
*
* @param string|null $str
*/
public function string($str = null): bool
{
return is_string($str);
}
/**
* Decimal number
*
* @param string|null $str
*/
public function decimal($str = null): bool
{
if (! is_string($str)) {
$str = (string) $str;
}
// @see https://regex101.com/r/HULifl/2/
return (bool) preg_match('/\A[-+]?\d{0,}\.?\d+\z/', $str);
}
/**
* String of hexidecimal characters
*
* @param string|null $str
*/
public function hex($str = null): bool
{
if (! is_string($str)) {
$str = (string) $str;
}
return ctype_xdigit($str);
}
/**
* Integer
*
* @param string|null $str
*/
public function integer($str = null): bool
{
if (! is_string($str)) {
$str = (string) $str;
}
return (bool) preg_match('/\A[\-+]?\d+\z/', $str);
}
/**
* Is a Natural number (0,1,2,3, etc.)
*
* @param string|null $str
*/
public function is_natural($str = null): bool
{
if (! is_string($str)) {
$str = (string) $str;
}
return ctype_digit($str);
}
/**
* Is a Natural number, but not a zero (1,2,3, etc.)
*
* @param string|null $str
*/
public function is_natural_no_zero($str = null): bool
{
if (! is_string($str)) {
$str = (string) $str;
}
return $str !== '0' && ctype_digit($str);
}
/**
* Numeric
*
* @param string|null $str
*/
public function numeric($str = null): bool
{
if (! is_string($str)) {
$str = (string) $str;
}
// @see https://regex101.com/r/bb9wtr/2
return (bool) preg_match('/\A[\-+]?\d*\.?\d+\z/', $str);
}
/**
* Compares value against a regular expression pattern.
*
* @param string|null $str
*/
public function regex_match($str, string $pattern): bool
{
if (! is_string($str)) {
$str = (string) $str;
}
if (! str_starts_with($pattern, '/')) {
$pattern = "/{$pattern}/";
}
return (bool) preg_match($pattern, $str);
}
/**
* Validates that the string is a valid timezone as per the
* timezone_identifiers_list function.
*
* @see http://php.net/manual/en/datetimezone.listidentifiers.php
*
* @param string|null $str
*/
public function timezone($str = null): bool
{
if (! is_string($str)) {
$str = (string) $str;
}
return in_array($str, timezone_identifiers_list(), true);
}
/**
* Valid Base64
*
* Tests a string for characters outside of the Base64 alphabet
* as defined by RFC 2045 http://www.faqs.org/rfcs/rfc2045
*
* @param string|null $str
*/
public function valid_base64($str = null): bool
{
if ($str === null) {
return false;
}
if (! is_string($str)) {
$str = (string) $str;
}
return base64_encode(base64_decode($str, true)) === $str;
}
/**
* Valid JSON
*
* @param string|null $str
*/
public function valid_json($str = null): bool
{
if (! is_string($str)) {
$str = (string) $str;
}
json_decode($str);
return json_last_error() === JSON_ERROR_NONE;
}
/**
* Checks for a correctly formatted email address
*
* @param string|null $str
*/
public function valid_email($str = null): bool
{
if (! is_string($str)) {
$str = (string) $str;
}
// @see https://regex101.com/r/wlJG1t/1/
if (function_exists('idn_to_ascii') && defined('INTL_IDNA_VARIANT_UTS46') && preg_match('#\A([^@]+)@(.+)\z#', $str, $matches)) {
$str = $matches[1] . '@' . idn_to_ascii($matches[2], 0, INTL_IDNA_VARIANT_UTS46);
}
return (bool) filter_var($str, FILTER_VALIDATE_EMAIL);
}
/**
* Validate a comma-separated list of email addresses.
*
* Example:
* valid_emails[one@example.com,two@example.com]
*
* @param string|null $str
*/
public function valid_emails($str = null): bool
{
if (! is_string($str)) {
$str = (string) $str;
}
foreach (explode(',', $str) as $email) {
$email = trim($email);
if ($email === '') {
return false;
}
if ($this->valid_email($email) === false) {
return false;
}
}
return true;
}
/**
* Validate an IP address (human readable format or binary string - inet_pton)
*
* @param string|null $ip
* @param string|null $which IP protocol: 'ipv4' or 'ipv6'
*/
public function valid_ip($ip = null, ?string $which = null): bool
{
if (! is_string($ip)) {
$ip = (string) $ip;
}
if ($ip === '') {
return false;
}
$option = match (strtolower($which ?? '')) {
'ipv4' => FILTER_FLAG_IPV4,
'ipv6' => FILTER_FLAG_IPV6,
default => 0,
};
return filter_var($ip, FILTER_VALIDATE_IP, $option) !== false
|| (! ctype_print($ip) && filter_var(inet_ntop($ip), FILTER_VALIDATE_IP, $option) !== false);
}
/**
* Checks a string to ensure it is (loosely) a URL.
*
* Warning: this rule will pass basic strings like
* "banana"; use valid_url_strict for a stricter rule.
*
* @param string|null $str
*/
public function valid_url($str = null): bool
{
if ($str === null || $str === '') {
return false;
}
if (! is_string($str)) {
$str = (string) $str;
}
if (preg_match('/\A(?:([^:]*)\:)?\/\/(.+)\z/', $str, $matches)) {
if (! in_array($matches[1], ['http', 'https'], true)) {
return false;
}
$str = $matches[2];
}
$str = 'http://' . $str;
return filter_var($str, FILTER_VALIDATE_URL) !== false;
}
/**
* Checks a URL to ensure it's formed correctly.
*
* @param string|null $str
* @param string|null $validSchemes comma separated list of allowed schemes
*/
public function valid_url_strict($str = null, ?string $validSchemes = null): bool
{
if ($str === null || $str === '' || $str === '0') {
return false;
}
if (! is_string($str)) {
$str = (string) $str;
}
// parse_url() may return null and false
$scheme = strtolower((string) parse_url($str, PHP_URL_SCHEME));
$validSchemes = explode(
',',
strtolower($validSchemes ?? 'http,https'),
);
return in_array($scheme, $validSchemes, true)
&& filter_var($str, FILTER_VALIDATE_URL) !== false;
}
/**
* Checks for a valid date and matches a given date format
*
* @param string|null $str
* @param non-empty-string|null $format
*/
public function valid_date($str = null, ?string $format = null): bool
{
if (! is_string($str)) {
$str = (string) $str;
}
if ($str === '') {
return false;
}
if ($format === null || $format === '') {
return strtotime($str) !== false;
}
$date = DateTime::createFromFormat($format, $str);
$errors = DateTime::getLastErrors();
if ($date === false) {
return false;
}
// PHP 8.2 or later.
if ($errors === false) {
return true;
}
return $errors['warning_count'] === 0 && $errors['error_count'] === 0;
}
}
@@ -0,0 +1,468 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Validation;
use CodeIgniter\Database\BaseBuilder;
use CodeIgniter\Exceptions\InvalidArgumentException;
use CodeIgniter\Helpers\Array\ArrayHelper;
use Config\Database;
/**
* Validation Rules.
*
* @see \CodeIgniter\Validation\RulesTest
*/
class Rules
{
/**
* The value does not match another field in $data.
*
* @param string|null $str
* @param array $data Other field/value pairs
*/
public function differs($str, string $field, array $data): bool
{
if (str_contains($field, '.')) {
return $str !== dot_array_search($field, $data);
}
return array_key_exists($field, $data) && $str !== $data[$field];
}
/**
* Equals the static value provided.
*
* @param string|null $str
*/
public function equals($str, string $val): bool
{
if (! is_string($str) && $str !== null) {
$str = (string) $str;
}
return $str === $val;
}
/**
* Returns true if $str is $val characters long.
* $val = "5" (one) | "5,8,12" (multiple values)
*
* @param string|null $str
*/
public function exact_length($str, string $val): bool
{
if (! is_string($str) && $str !== null) {
$str = (string) $str;
}
$val = explode(',', $val);
foreach ($val as $tmp) {
if (is_numeric($tmp) && (int) $tmp === mb_strlen($str ?? '')) {
return true;
}
}
return false;
}
/**
* Greater than
*
* @param string|null $str
*/
public function greater_than($str, string $min): bool
{
if (! is_string($str) && $str !== null) {
$str = (string) $str;
}
return is_numeric($str) && $str > $min;
}
/**
* Equal to or Greater than
*
* @param string|null $str
*/
public function greater_than_equal_to($str, string $min): bool
{
if (! is_string($str) && $str !== null) {
$str = (string) $str;
}
return is_numeric($str) && $str >= $min;
}
/**
* Checks the database to see if the given value exist.
* Can ignore records by field/value to filter (currently
* accept only one filter).
*
* Example:
* is_not_unique[dbGroup.table.field,where_field,where_value]
* is_not_unique[table.field,where_field,where_value]
* is_not_unique[menu.id,active,1]
*
* @param string|null $str
*/
public function is_not_unique($str, string $field, array $data): bool
{
[$builder, $whereField, $whereValue] = $this->prepareUniqueQuery($str, $field, $data);
if (
$whereField !== null && $whereField !== ''
&& $whereValue !== null && $whereValue !== ''
&& preg_match('/^\{(\w+)\}$/', $whereValue) !== 1
) {
$builder = $builder->where($whereField, $whereValue);
}
return $builder->get()->getRow() !== null;
}
/**
* Value should be within an array of values
*
* @param string|null $value
*/
public function in_list($value, string $list): bool
{
if (! is_string($value) && $value !== null) {
$value = (string) $value;
}
$list = array_map(trim(...), explode(',', $list));
return in_array($value, $list, true);
}
/**
* Checks the database to see if the given value is unique. Can
* ignore a single record by field/value to make it useful during
* record updates.
*
* Example:
* is_unique[dbGroup.table.field,ignore_field,ignore_value]
* is_unique[table.field,ignore_field,ignore_value]
* is_unique[users.email,id,5]
*
* @param string|null $str
*/
public function is_unique($str, string $field, array $data): bool
{
[$builder, $ignoreField, $ignoreValue] = $this->prepareUniqueQuery($str, $field, $data);
if (
$ignoreField !== null && $ignoreField !== ''
&& $ignoreValue !== null && $ignoreValue !== ''
&& preg_match('/^\{(\w+)\}$/', $ignoreValue) !== 1
) {
$builder = $builder->where("{$ignoreField} !=", $ignoreValue);
}
return $builder->get()->getRow() === null;
}
/**
* Prepares the database query for uniqueness checks.
*
* @param mixed $value The value to check.
* @param string $field The field parameters.
* @param array<string, mixed> $data Additional data.
*
* @return array{0: BaseBuilder, 1: string|null, 2: string|null}
*/
private function prepareUniqueQuery($value, string $field, array $data): array
{
if (! is_string($value) && $value !== null) {
$value = (string) $value;
}
// Split the field parameters and pad the array to ensure three elements.
[$field, $extraField, $extraValue] = array_pad(explode(',', $field), 3, null);
// Parse the field string to extract dbGroup, table, and field.
$parts = explode('.', $field, 3);
$numParts = count($parts);
if ($numParts === 3) {
[$dbGroup, $table, $field] = $parts;
} elseif ($numParts === 2) {
[$table, $field] = $parts;
} else {
throw new InvalidArgumentException('The field must be in the format "table.field" or "dbGroup.table.field".');
}
// Connect to the database.
$dbGroup ??= $data['DBGroup'] ?? null;
$builder = Database::connect($dbGroup)
->table($table)
->select('1')
->where($field, $value)
->limit(1);
return [$builder, $extraField, $extraValue];
}
/**
* Less than
*
* @param string|null $str
*/
public function less_than($str, string $max): bool
{
if (! is_string($str) && $str !== null) {
$str = (string) $str;
}
return is_numeric($str) && $str < $max;
}
/**
* Equal to or Less than
*
* @param string|null $str
*/
public function less_than_equal_to($str, string $max): bool
{
if (! is_string($str) && $str !== null) {
$str = (string) $str;
}
return is_numeric($str) && $str <= $max;
}
/**
* Matches the value of another field in $data.
*
* @param string|null $str
* @param array $data Other field/value pairs
*/
public function matches($str, string $field, array $data): bool
{
if (str_contains($field, '.')) {
return $str === dot_array_search($field, $data);
}
return isset($data[$field]) && $str === $data[$field];
}
/**
* Returns true if $str is $val or fewer characters in length.
*
* @param string|null $str
*/
public function max_length($str, string $val): bool
{
if (! is_string($str) && $str !== null) {
$str = (string) $str;
}
return is_numeric($val) && $val >= mb_strlen($str ?? '');
}
/**
* Returns true if $str is at least $val length.
*
* @param string|null $str
*/
public function min_length($str, string $val): bool
{
if (! is_string($str) && $str !== null) {
$str = (string) $str;
}
return is_numeric($val) && $val <= mb_strlen($str ?? '');
}
/**
* Does not equal the static value provided.
*
* @param string|null $str
*/
public function not_equals($str, string $val): bool
{
if (! is_string($str) && $str !== null) {
$str = (string) $str;
}
return $str !== $val;
}
/**
* Value should not be within an array of values.
*
* @param string|null $value
*/
public function not_in_list($value, string $list): bool
{
if (! is_string($value) && $value !== null) {
$value = (string) $value;
}
return ! $this->in_list($value, $list);
}
/**
* @param array|bool|float|int|object|string|null $str
*/
public function required($str = null): bool
{
if ($str === null) {
return false;
}
if (is_object($str)) {
return true;
}
if (is_array($str)) {
return $str !== [];
}
return trim((string) $str) !== '';
}
/**
* The field is required when any of the other required fields are present
* in the data.
*
* Example (field is required when the password field is present):
*
* required_with[password]
*
* @param string|null $str
* @param string|null $fields List of fields that we should check if present
* @param array $data Complete list of fields from the form
*/
public function required_with($str = null, ?string $fields = null, array $data = []): bool
{
if ($fields === null || $data === []) {
throw new InvalidArgumentException('You must supply the parameters: fields, data.');
}
// If the field is present we can safely assume that
// the field is here, no matter whether the corresponding
// search field is present or not.
$present = $this->required($str ?? '');
if ($present) {
return true;
}
// Still here? Then we fail this test if
// any of the fields are present in $data
// as $fields is the list
$requiredFields = [];
foreach (explode(',', $fields) as $field) {
if (
(array_key_exists($field, $data) && ! empty($data[$field]))
|| (str_contains($field, '.') && ! empty(dot_array_search($field, $data)))
) {
$requiredFields[] = $field;
}
}
return $requiredFields === [];
}
/**
* The field is required when all the other fields are present
* in the data but not required.
*
* Example (field is required when the id or email field is missing):
*
* required_without[id,email]
*
* @param string|null $str
* @param string|null $otherFields The param fields of required_without[].
* @param string|null $field This rule param fields aren't present, this field is required.
*/
public function required_without(
$str = null,
?string $otherFields = null,
array $data = [],
?string $error = null,
?string $field = null,
): bool {
if ($otherFields === null || $data === []) {
throw new InvalidArgumentException('You must supply the parameters: otherFields, data.');
}
// If the field is present we can safely assume that
// the field is here, no matter whether the corresponding
// search field is present or not.
$present = $this->required($str ?? '');
if ($present) {
return true;
}
// Still here? Then we fail this test if
// any of the fields are not present in $data
foreach (explode(',', $otherFields) as $otherField) {
if (
(! str_contains($otherField, '.'))
&& (! array_key_exists($otherField, $data)
|| empty($data[$otherField]))
) {
return false;
}
if (str_contains($otherField, '.')) {
if ($field === null) {
throw new InvalidArgumentException('You must supply the parameters: field.');
}
$fieldData = dot_array_search($otherField, $data);
$fieldSplitArray = explode('.', $field);
$fieldKey = $fieldSplitArray[1];
if (is_array($fieldData)) {
return ! empty(dot_array_search($otherField, $data)[$fieldKey]);
}
$nowField = str_replace('*', $fieldKey, $otherField);
$nowFieldVaule = dot_array_search($nowField, $data);
return null !== $nowFieldVaule;
}
}
return true;
}
/**
* The field exists in $data.
*
* @param array|bool|float|int|object|string|null $value The field value.
* @param string|null $param The rule's parameter.
* @param array $data The data to be validated.
* @param string|null $field The field name.
*/
public function field_exists(
$value = null,
?string $param = null,
array $data = [],
?string $error = null,
?string $field = null,
): bool {
if (str_contains($field, '.')) {
return ArrayHelper::dotKeyExists($field, $data);
}
return array_key_exists($field, $data);
}
}
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Validation\StrictRules;
use CodeIgniter\Validation\CreditCardRules as NonStrictCreditCardRules;
/**
* Class CreditCardRules
*
* Provides validation methods for common credit-card inputs.
*
* @see http://en.wikipedia.org/wiki/Credit_card_number
* @see \CodeIgniter\Validation\StrictRules\CreditCardRulesTest
*/
class CreditCardRules
{
private readonly NonStrictCreditCardRules $nonStrictCreditCardRules;
public function __construct()
{
$this->nonStrictCreditCardRules = new NonStrictCreditCardRules();
}
/**
* Verifies that a credit card number is valid and matches the known
* formats for a wide number of credit card types. This does not verify
* that the card is a valid card, only that the number is formatted correctly.
*
* Example:
* $rules = [
* 'cc_num' => 'valid_cc_number[visa]'
* ];
*
* @param array|bool|float|int|object|string|null $ccNumber
*/
public function valid_cc_number($ccNumber, string $type): bool
{
if (! is_string($ccNumber)) {
return false;
}
return $this->nonStrictCreditCardRules->valid_cc_number($ccNumber, $type);
}
}
@@ -0,0 +1,317 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Validation\StrictRules;
use CodeIgniter\Exceptions\InvalidArgumentException;
use CodeIgniter\HTTP\CLIRequest;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\RequestInterface;
use Config\Mimes;
/**
* File validation rules
*
* @see \CodeIgniter\Validation\StrictRules\FileRulesTest
*/
class FileRules
{
/**
* Request instance. So we can get access to the files.
*
* @var IncomingRequest
*/
protected $request;
/**
* Constructor.
*/
public function __construct(?RequestInterface $request = null)
{
if (! $request instanceof RequestInterface) {
$request = service('request');
}
assert($request instanceof IncomingRequest || $request instanceof CLIRequest);
$this->request = $request;
}
/**
* Verifies that $name is the name of a valid uploaded file.
*/
public function uploaded(?string $blank, string $name): bool
{
$files = $this->request->getFileMultiple($name);
if ($files === null) {
$files = [$this->request->getFile($name)];
}
foreach ($files as $file) {
if ($file === null) {
return false;
}
if (ENVIRONMENT === 'testing') {
if ($file->getError() !== 0) {
return false;
}
} else {
// Note: cannot unit test this; no way to over-ride ENVIRONMENT?
// @codeCoverageIgnoreStart
if (! $file->isValid()) {
return false;
}
// @codeCoverageIgnoreEnd
}
}
return true;
}
/**
* Verifies if the file's size in Kilobytes is no larger than the parameter.
*/
public function max_size(?string $blank, string $params): bool
{
// Grab the file name off the top of the $params
// after we split it.
$paramArray = explode(',', $params);
if (count($paramArray) !== 2) {
throw new InvalidArgumentException('Invalid max_size parameter: "' . $params . '"');
}
$name = array_shift($paramArray);
$files = $this->request->getFileMultiple($name);
if ($files === null) {
$files = [$this->request->getFile($name)];
}
foreach ($files as $file) {
if ($file === null) {
return false;
}
if ($file->getError() === UPLOAD_ERR_NO_FILE) {
return true;
}
if ($file->getError() === UPLOAD_ERR_INI_SIZE) {
return false;
}
if ($file->getSize() / 1024 > $paramArray[0]) {
return false;
}
}
return true;
}
/**
* Uses the mime config file to determine if a file is considered an "image",
* which for our purposes basically means that it's a raster image or svg.
*/
public function is_image(?string $blank, string $params): bool
{
// Grab the file name off the top of the $params
// after we split it.
$params = explode(',', $params);
$name = array_shift($params);
$files = $this->request->getFileMultiple($name);
if ($files === null) {
$files = [$this->request->getFile($name)];
}
foreach ($files as $file) {
if ($file === null) {
return false;
}
if ($file->getError() === UPLOAD_ERR_NO_FILE) {
return true;
}
// We know that our mimes list always has the first mime
// start with `image` even when then are multiple accepted types.
$type = Mimes::guessTypeFromExtension($file->getExtension()) ?? '';
if (mb_strpos($type, 'image') !== 0) {
return false;
}
}
return true;
}
/**
* Checks to see if an uploaded file's mime type matches one in the parameter.
*/
public function mime_in(?string $blank, string $params): bool
{
// Grab the file name off the top of the $params
// after we split it.
$params = explode(',', $params);
$name = array_shift($params);
$files = $this->request->getFileMultiple($name);
if ($files === null) {
$files = [$this->request->getFile($name)];
}
foreach ($files as $file) {
if ($file === null) {
return false;
}
if ($file->getError() === UPLOAD_ERR_NO_FILE) {
return true;
}
if (! in_array($file->getMimeType(), $params, true)) {
return false;
}
}
return true;
}
/**
* Checks to see if an uploaded file's extension matches one in the parameter.
*/
public function ext_in(?string $blank, string $params): bool
{
// Grab the file name off the top of the $params
// after we split it.
$params = explode(',', $params);
$name = array_shift($params);
$files = $this->request->getFileMultiple($name);
if ($files === null) {
$files = [$this->request->getFile($name)];
}
foreach ($files as $file) {
if ($file === null) {
return false;
}
if ($file->getError() === UPLOAD_ERR_NO_FILE) {
return true;
}
if (! in_array($file->guessExtension(), $params, true)) {
return false;
}
}
return true;
}
/**
* Checks an uploaded file to verify that the dimensions are within
* a specified allowable dimension.
*/
public function max_dims(?string $blank, string $params): bool
{
// Grab the file name off the top of the $params
// after we split it.
$params = explode(',', $params);
$name = array_shift($params);
$files = $this->request->getFileMultiple($name);
if ($files === null) {
$files = [$this->request->getFile($name)];
}
foreach ($files as $file) {
if ($file === null) {
return false;
}
if ($file->getError() === UPLOAD_ERR_NO_FILE) {
return true;
}
// Get Parameter sizes
$allowedWidth = $params[0] ?? 0;
$allowedHeight = $params[1] ?? 0;
// Get uploaded image size
$info = getimagesize($file->getTempName());
if ($info === false) {
// Cannot get the image size.
return false;
}
$fileWidth = $info[0];
$fileHeight = $info[1];
if ($fileWidth > $allowedWidth || $fileHeight > $allowedHeight) {
return false;
}
}
return true;
}
/**
* Checks an uploaded file to verify that the dimensions are greater than
* a specified dimension.
*/
public function min_dims(?string $blank, string $params): bool
{
// Grab the file name off the top of the $params
// after we split it.
$params = explode(',', $params);
$name = array_shift($params);
$files = $this->request->getFileMultiple($name);
if ($files === null) {
$files = [$this->request->getFile($name)];
}
foreach ($files as $file) {
if ($file === null) {
return false;
}
if ($file->getError() === UPLOAD_ERR_NO_FILE) {
return true;
}
// Get Parameter sizes
$minimumWidth = $params[0] ?? 0;
$minimumHeight = $params[1] ?? 0;
// Get uploaded image size
$info = getimagesize($file->getTempName());
if ($info === false) {
// Cannot get the image size.
return false;
}
$fileWidth = $info[0];
$fileHeight = $info[1];
if ($fileWidth < $minimumWidth || $fileHeight < $minimumHeight) {
return false;
}
}
return true;
}
}
@@ -0,0 +1,411 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Validation\StrictRules;
use CodeIgniter\Validation\FormatRules as NonStrictFormatRules;
/**
* Format validation Rules.
*
* @see \CodeIgniter\Validation\StrictRules\FormatRulesTest
*/
class FormatRules
{
private readonly NonStrictFormatRules $nonStrictFormatRules;
public function __construct()
{
$this->nonStrictFormatRules = new NonStrictFormatRules();
}
/**
* Alpha
*
* @param array|bool|float|int|object|string|null $str
*/
public function alpha($str = null): bool
{
if (! is_string($str)) {
return false;
}
return $this->nonStrictFormatRules->alpha($str);
}
/**
* Alpha with spaces.
*
* @param array|bool|float|int|object|string|null $value Value.
*
* @return bool True if alpha with spaces, else false.
*/
public function alpha_space($value = null): bool
{
if (! is_string($value)) {
return false;
}
return $this->nonStrictFormatRules->alpha_space($value);
}
/**
* Alphanumeric with underscores and dashes
*
* @param array|bool|float|int|object|string|null $str
*/
public function alpha_dash($str = null): bool
{
if (is_int($str)) {
$str = (string) $str;
}
if (! is_string($str)) {
return false;
}
return $this->nonStrictFormatRules->alpha_dash($str);
}
/**
* Alphanumeric, spaces, and a limited set of punctuation characters.
* Accepted punctuation characters are: ~ tilde, ! exclamation,
* # number, $ dollar, % percent, & ampersand, * asterisk, - dash,
* _ underscore, + plus, = equals, | vertical bar, : colon, . period
* ~ ! # $ % & * - _ + = | : .
*
* @param array|bool|float|int|object|string|null $str
*
* @return bool
*/
public function alpha_numeric_punct($str)
{
if (is_int($str) || is_float($str)) {
$str = (string) $str;
}
if (! is_string($str)) {
return false;
}
return $this->nonStrictFormatRules->alpha_numeric_punct($str);
}
/**
* Alphanumeric
*
* @param array|bool|float|int|object|string|null $str
*/
public function alpha_numeric($str = null): bool
{
if (is_int($str)) {
$str = (string) $str;
}
if (! is_string($str)) {
return false;
}
return $this->nonStrictFormatRules->alpha_numeric($str);
}
/**
* Alphanumeric w/ spaces
*
* @param array|bool|float|int|object|string|null $str
*/
public function alpha_numeric_space($str = null): bool
{
if (is_int($str)) {
$str = (string) $str;
}
if (! is_string($str)) {
return false;
}
return $this->nonStrictFormatRules->alpha_numeric_space($str);
}
/**
* Any type of string
*
* @param array|bool|float|int|object|string|null $str
*/
public function string($str = null): bool
{
return $this->nonStrictFormatRules->string($str);
}
/**
* Decimal number
*
* @param array|bool|float|int|object|string|null $str
*/
public function decimal($str = null): bool
{
if (is_int($str) || is_float($str)) {
$str = (string) $str;
}
if (! is_string($str)) {
return false;
}
return $this->nonStrictFormatRules->decimal($str);
}
/**
* String of hexidecimal characters
*
* @param array|bool|float|int|object|string|null $str
*/
public function hex($str = null): bool
{
if (is_int($str)) {
$str = (string) $str;
}
if (! is_string($str)) {
return false;
}
return $this->nonStrictFormatRules->hex($str);
}
/**
* Integer
*
* @param array|bool|float|int|object|string|null $str
*/
public function integer($str = null): bool
{
if (is_int($str)) {
$str = (string) $str;
}
if (! is_string($str)) {
return false;
}
return $this->nonStrictFormatRules->integer($str);
}
/**
* Is a Natural number (0,1,2,3, etc.)
*
* @param array|bool|float|int|object|string|null $str
*/
public function is_natural($str = null): bool
{
if (is_int($str)) {
$str = (string) $str;
}
if (! is_string($str)) {
return false;
}
return $this->nonStrictFormatRules->is_natural($str);
}
/**
* Is a Natural number, but not a zero (1,2,3, etc.)
*
* @param array|bool|float|int|object|string|null $str
*/
public function is_natural_no_zero($str = null): bool
{
if (is_int($str)) {
$str = (string) $str;
}
if (! is_string($str)) {
return false;
}
return $this->nonStrictFormatRules->is_natural_no_zero($str);
}
/**
* Numeric
*
* @param array|bool|float|int|object|string|null $str
*/
public function numeric($str = null): bool
{
if (is_int($str) || is_float($str)) {
$str = (string) $str;
}
if (! is_string($str)) {
return false;
}
return $this->nonStrictFormatRules->numeric($str);
}
/**
* Compares value against a regular expression pattern.
*
* @param array|bool|float|int|object|string|null $str
*/
public function regex_match($str, string $pattern): bool
{
if (! is_string($str)) {
return false;
}
return $this->nonStrictFormatRules->regex_match($str, $pattern);
}
/**
* Validates that the string is a valid timezone as per the
* timezone_identifiers_list function.
*
* @see http://php.net/manual/en/datetimezone.listidentifiers.php
*
* @param array|bool|float|int|object|string|null $str
*/
public function timezone($str = null): bool
{
if (! is_string($str)) {
return false;
}
return $this->nonStrictFormatRules->timezone($str);
}
/**
* Valid Base64
*
* Tests a string for characters outside of the Base64 alphabet
* as defined by RFC 2045 http://www.faqs.org/rfcs/rfc2045
*
* @param array|bool|float|int|object|string|null $str
*/
public function valid_base64($str = null): bool
{
if (! is_string($str)) {
return false;
}
return $this->nonStrictFormatRules->valid_base64($str);
}
/**
* Valid JSON
*
* @param array|bool|float|int|object|string|null $str
*/
public function valid_json($str = null): bool
{
if (! is_string($str)) {
return false;
}
return $this->nonStrictFormatRules->valid_json($str);
}
/**
* Checks for a correctly formatted email address
*
* @param array|bool|float|int|object|string|null $str
*/
public function valid_email($str = null): bool
{
if (! is_string($str)) {
return false;
}
return $this->nonStrictFormatRules->valid_email($str);
}
/**
* Validate a comma-separated list of email addresses.
*
* Example:
* valid_emails[one@example.com,two@example.com]
*
* @param array|bool|float|int|object|string|null $str
*/
public function valid_emails($str = null): bool
{
if (! is_string($str)) {
return false;
}
return $this->nonStrictFormatRules->valid_emails($str);
}
/**
* Validate an IP address (human readable format or binary string - inet_pton)
*
* @param array|bool|float|int|object|string|null $ip
* @param string|null $which IP protocol: 'ipv4' or 'ipv6'
*/
public function valid_ip($ip = null, ?string $which = null): bool
{
if (! is_string($ip)) {
return false;
}
return $this->nonStrictFormatRules->valid_ip($ip, $which);
}
/**
* Checks a string to ensure it is (loosely) a URL.
*
* Warning: this rule will pass basic strings like
* "banana"; use valid_url_strict for a stricter rule.
*
* @param array|bool|float|int|object|string|null $str
*/
public function valid_url($str = null): bool
{
if (! is_string($str)) {
return false;
}
return $this->nonStrictFormatRules->valid_url($str);
}
/**
* Checks a URL to ensure it's formed correctly.
*
* @param array|bool|float|int|object|string|null $str
* @param string|null $validSchemes comma separated list of allowed schemes
*/
public function valid_url_strict($str = null, ?string $validSchemes = null): bool
{
if (! is_string($str)) {
return false;
}
return $this->nonStrictFormatRules->valid_url_strict($str, $validSchemes);
}
/**
* Checks for a valid date and matches a given date format
*
* @param array|bool|float|int|object|string|null $str
*/
public function valid_date($str = null, ?string $format = null): bool
{
if (! is_string($str)) {
return false;
}
return $this->nonStrictFormatRules->valid_date($str, $format);
}
}
@@ -0,0 +1,394 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Validation\StrictRules;
use CodeIgniter\Helpers\Array\ArrayHelper;
use CodeIgniter\Validation\Rules as NonStrictRules;
/**
* Validation Rules.
*
* @see \CodeIgniter\Validation\StrictRules\RulesTest
*/
class Rules
{
private readonly NonStrictRules $nonStrictRules;
public function __construct()
{
$this->nonStrictRules = new NonStrictRules();
}
/**
* The value does not match another field in $data.
*
* @param array|bool|float|int|object|string|null $str
* @param array $data Other field/value pairs
*/
public function differs(
$str,
string $otherField,
array $data,
?string $error = null,
?string $field = null,
): bool {
if (str_contains($otherField, '.')) {
return $str !== dot_array_search($otherField, $data);
}
if (! array_key_exists($otherField, $data)) {
return false;
}
if (str_contains($field, '.')) {
if (! ArrayHelper::dotKeyExists($field, $data)) {
return false;
}
} elseif (! array_key_exists($field, $data)) {
return false;
}
return $str !== ($data[$otherField] ?? null);
}
/**
* Equals the static value provided.
*
* @param array|bool|float|int|object|string|null $str
*/
public function equals($str, string $val): bool
{
return $this->nonStrictRules->equals($str, $val);
}
/**
* Returns true if $str is $val characters long.
* $val = "5" (one) | "5,8,12" (multiple values)
*
* @param array|bool|float|int|object|string|null $str
*/
public function exact_length($str, string $val): bool
{
if (is_int($str) || is_float($str)) {
$str = (string) $str;
}
if (! is_string($str)) {
return false;
}
return $this->nonStrictRules->exact_length($str, $val);
}
/**
* Greater than
*
* @param array|bool|float|int|object|string|null $str expects int|string
*/
public function greater_than($str, string $min): bool
{
if (is_int($str) || is_float($str)) {
$str = (string) $str;
}
if (! is_string($str)) {
return false;
}
return $this->nonStrictRules->greater_than($str, $min);
}
/**
* Equal to or Greater than
*
* @param array|bool|float|int|object|string|null $str expects int|string
*/
public function greater_than_equal_to($str, string $min): bool
{
if (is_int($str) || is_float($str)) {
$str = (string) $str;
}
if (! is_string($str)) {
return false;
}
return $this->nonStrictRules->greater_than_equal_to($str, $min);
}
/**
* Checks the database to see if the given value exist.
* Can ignore records by field/value to filter (currently
* accept only one filter).
*
* Example:
* is_not_unique[dbGroup.table.field,where_field,where_value]
* is_not_unique[table.field,where_field,where_value]
* is_not_unique[menu.id,active,1]
*
* @param array|bool|float|int|object|string|null $str
*/
public function is_not_unique($str, string $field, array $data): bool
{
if (is_object($str) || is_array($str)) {
return false;
}
return $this->nonStrictRules->is_not_unique($str, $field, $data);
}
/**
* Value should be within an array of values
*
* @param array|bool|float|int|object|string|null $value
*/
public function in_list($value, string $list): bool
{
if (is_int($value) || is_float($value)) {
$value = (string) $value;
}
if (! is_string($value)) {
return false;
}
return $this->nonStrictRules->in_list($value, $list);
}
/**
* Checks the database to see if the given value is unique. Can
* ignore a single record by field/value to make it useful during
* record updates.
*
* Example:
* is_unique[dbGroup.table.field,ignore_field,ignore_value]
* is_unique[table.field,ignore_field,ignore_value]
* is_unique[users.email,id,5]
*
* @param array|bool|float|int|object|string|null $str
*/
public function is_unique($str, string $field, array $data): bool
{
if (is_object($str) || is_array($str)) {
return false;
}
return $this->nonStrictRules->is_unique($str, $field, $data);
}
/**
* Less than
*
* @param array|bool|float|int|object|string|null $str expects int|string
*/
public function less_than($str, string $max): bool
{
if (is_int($str) || is_float($str)) {
$str = (string) $str;
}
if (! is_string($str)) {
return false;
}
return $this->nonStrictRules->less_than($str, $max);
}
/**
* Equal to or Less than
*
* @param array|bool|float|int|object|string|null $str expects int|string
*/
public function less_than_equal_to($str, string $max): bool
{
if (is_int($str) || is_float($str)) {
$str = (string) $str;
}
if (! is_string($str)) {
return false;
}
return $this->nonStrictRules->less_than_equal_to($str, $max);
}
/**
* Matches the value of another field in $data.
*
* @param array|bool|float|int|object|string|null $str
* @param array $data Other field/value pairs
*/
public function matches(
$str,
string $otherField,
array $data,
?string $error = null,
?string $field = null,
): bool {
if (str_contains($otherField, '.')) {
return $str === dot_array_search($otherField, $data);
}
if (! array_key_exists($otherField, $data)) {
return false;
}
if (str_contains($field, '.')) {
if (! ArrayHelper::dotKeyExists($field, $data)) {
return false;
}
} elseif (! array_key_exists($field, $data)) {
return false;
}
return $str === ($data[$otherField] ?? null);
}
/**
* Returns true if $str is $val or fewer characters in length.
*
* @param array|bool|float|int|object|string|null $str
*/
public function max_length($str, string $val): bool
{
if (is_int($str) || is_float($str) || null === $str) {
$str = (string) $str;
}
if (! is_string($str)) {
return false;
}
return $this->nonStrictRules->max_length($str, $val);
}
/**
* Returns true if $str is at least $val length.
*
* @param array|bool|float|int|object|string|null $str
*/
public function min_length($str, string $val): bool
{
if (is_int($str) || is_float($str)) {
$str = (string) $str;
}
if (! is_string($str)) {
return false;
}
return $this->nonStrictRules->min_length($str, $val);
}
/**
* Does not equal the static value provided.
*
* @param array|bool|float|int|object|string|null $str
*/
public function not_equals($str, string $val): bool
{
return $this->nonStrictRules->not_equals($str, $val);
}
/**
* Value should not be within an array of values.
*
* @param array|bool|float|int|object|string|null $value
*/
public function not_in_list($value, string $list): bool
{
if (null === $value) {
return true;
}
if (is_int($value) || is_float($value)) {
$value = (string) $value;
}
if (! is_string($value)) {
return false;
}
return $this->nonStrictRules->not_in_list($value, $list);
}
/**
* @param array|bool|float|int|object|string|null $str
*/
public function required($str = null): bool
{
return $this->nonStrictRules->required($str);
}
/**
* The field is required when any of the other required fields are present
* in the data.
*
* Example (field is required when the password field is present):
*
* required_with[password]
*
* @param array|bool|float|int|object|string|null $str
* @param string|null $fields List of fields that we should check if present
* @param array $data Complete list of fields from the form
*/
public function required_with($str = null, ?string $fields = null, array $data = []): bool
{
return $this->nonStrictRules->required_with($str, $fields, $data);
}
/**
* The field is required when all the other fields are present
* in the data but not required.
*
* Example (field is required when the id or email field is missing):
*
* required_without[id,email]
*
* @param array|bool|float|int|object|string|null $str
* @param string|null $otherFields The param fields of required_without[].
* @param string|null $field This rule param fields aren't present, this field is required.
*/
public function required_without(
$str = null,
?string $otherFields = null,
array $data = [],
?string $error = null,
?string $field = null,
): bool {
return $this->nonStrictRules->required_without($str, $otherFields, $data, $error, $field);
}
/**
* The field exists in $data.
*
* @param array|bool|float|int|object|string|null $value The field value.
* @param string|null $param The rule's parameter.
* @param array $data The data to be validated.
* @param string|null $field The field name.
*/
public function field_exists(
$value = null,
?string $param = null,
array $data = [],
?string $error = null,
?string $field = null,
): bool {
if (str_contains($field, '.')) {
return ArrayHelper::dotKeyExists($field, $data);
}
return array_key_exists($field, $data);
}
}
@@ -0,0 +1,994 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Validation;
use Closure;
use CodeIgniter\Database\BaseConnection;
use CodeIgniter\Exceptions\InvalidArgumentException;
use CodeIgniter\Exceptions\LogicException;
use CodeIgniter\HTTP\Exceptions\HTTPException;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\Method;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\Validation\Exceptions\ValidationException;
use CodeIgniter\View\RendererInterface;
/**
* Validator
*
* @see \CodeIgniter\Validation\ValidationTest
*/
class Validation implements ValidationInterface
{
/**
* Files to load with validation functions.
*
* @var array
*/
protected $ruleSetFiles;
/**
* The loaded instances of our validation files.
*
* @var array
*/
protected $ruleSetInstances = [];
/**
* Stores the actual rules that should be run against $data.
*
* @var array<array-key, array{label?: string, rules: list<string>}>
*
* [
* field1 => [
* 'label' => label,
* 'rules' => [
* rule1, rule2, ...
* ],
* ],
* ]
*/
protected $rules = [];
/**
* The data that should be validated,
* where 'key' is the alias, with value.
*
* @var array
*/
protected $data = [];
/**
* The data that was actually validated.
*
* @var array
*/
protected $validated = [];
/**
* Any generated errors during validation.
* 'key' is the alias, 'value' is the message.
*
* @var array
*/
protected $errors = [];
/**
* Stores custom error message to use
* during validation. Where 'key' is the alias.
*
* @var array
*/
protected $customErrors = [];
/**
* Our configuration.
*
* @var object{ruleSets: list<class-string>}
*/
protected $config;
/**
* The view renderer used to render validation messages.
*
* @var RendererInterface
*/
protected $view;
/**
* Validation constructor.
*
* @param object{ruleSets: list<class-string>} $config
*/
public function __construct($config, RendererInterface $view)
{
$this->ruleSetFiles = $config->ruleSets;
$this->config = $config;
$this->view = $view;
$this->loadRuleSets();
}
/**
* Runs the validation process, returning true/false determining whether
* validation was successful or not.
*
* @param array|null $data The array of data to validate.
* @param string|null $group The predefined group of rules to apply.
* @param array|BaseConnection|non-empty-string|null $dbGroup The database group to use.
*/
public function run(?array $data = null, ?string $group = null, $dbGroup = null): bool
{
if ($data === null) {
$data = $this->data;
} else {
// Store data to validate.
$this->data = $data;
}
// `DBGroup` is a reserved name. For is_unique and is_not_unique
$data['DBGroup'] = $dbGroup;
$this->loadRuleGroup($group);
// If no rules exist, we return false to ensure
// the developer didn't forget to set the rules.
if ($this->rules === []) {
return false;
}
// Replace any placeholders (e.g. {id}) in the rules with
// the value found in $data, if any.
$this->rules = $this->fillPlaceholders($this->rules, $data);
// Need this for searching arrays in validation.
helper('array');
// Run through each rule. If we have any field set for
// this rule, then we need to run them through!
foreach ($this->rules as $field => $setup) {
// An array key might be int.
$field = (string) $field;
$rules = $setup['rules'];
if (is_string($rules)) {
$rules = $this->splitRules($rules);
}
if (str_contains($field, '*')) {
$flattenedArray = array_flatten_with_dots($data);
$values = array_filter(
$flattenedArray,
static fn ($key): bool => preg_match(self::getRegex($field), $key) === 1,
ARRAY_FILTER_USE_KEY,
);
// if keys not found
$values = $values !== [] ? $values : [$field => null];
} else {
$values = dot_array_search($field, $data);
}
if ($values === []) {
// We'll process the values right away if an empty array
$this->processRules($field, $setup['label'] ?? $field, $values, $rules, $data, $field);
continue;
}
if (str_contains($field, '*')) {
// Process multiple fields
foreach ($values as $dotField => $value) {
$this->processRules($dotField, $setup['label'] ?? $field, $value, $rules, $data, $field);
}
} else {
// Process single field
$this->processRules($field, $setup['label'] ?? $field, $values, $rules, $data, $field);
}
}
if ($this->getErrors() === []) {
// Store data that was actually validated.
$this->validated = DotArrayFilter::run(
array_keys($this->rules),
$this->data,
);
return true;
}
return false;
}
/**
* Returns regex pattern for key with dot array syntax.
*/
private static function getRegex(string $field): string
{
return '/\A'
. str_replace(
['\.\*', '\*\.'],
['\.[^.]+', '[^.]+\.'],
preg_quote($field, '/'),
)
. '\z/';
}
/**
* Runs the validation process, returning true or false determining whether
* validation was successful or not.
*
* @param array|bool|float|int|object|string|null $value The data to validate.
* @param array|string $rules The validation rules.
* @param list<string> $errors The custom error message.
* @param string|null $dbGroup The database group to use.
*/
public function check($value, $rules, array $errors = [], $dbGroup = null): bool
{
$this->reset();
return $this->setRule(
'check',
null,
$rules,
$errors,
)->run(
['check' => $value],
null,
$dbGroup,
);
}
/**
* Returns the actual validated data.
*/
public function getValidated(): array
{
return $this->validated;
}
/**
* Runs all of $rules against $field, until one fails, or
* all of them have been processed. If one fails, it adds
* the error to $this->errors and moves on to the next,
* so that we can collect all of the first errors.
*
* @param array|string $value
* @param array $rules
* @param array $data The array of data to validate, with `DBGroup`.
* @param string|null $originalField The original asterisk field name like "foo.*.bar".
*/
protected function processRules(
string $field,
?string $label,
$value,
$rules = null, // @TODO remove `= null`
?array $data = null, // @TODO remove `= null`
?string $originalField = null,
): bool {
if ($data === null) {
throw new InvalidArgumentException('You must supply the parameter: data.');
}
$rules = $this->processIfExist($field, $rules, $data);
if ($rules === true) {
return true;
}
$rules = $this->processPermitEmpty($value, $rules, $data);
if ($rules === true) {
return true;
}
foreach ($rules as $i => $rule) {
$isCallable = is_callable($rule);
$stringCallable = $isCallable && is_string($rule);
$arrayCallable = $isCallable && is_array($rule);
$passed = false;
/** @var string|null $param */
$param = null;
if (! $isCallable && preg_match('/(.*?)\[(.*)\]/', $rule, $match)) {
$rule = $match[1];
$param = $match[2];
}
// Placeholder for custom errors from the rules.
$error = null;
// If it's a callable, call and get out of here.
if ($this->isClosure($rule)) {
$passed = $rule($value, $data, $error, $field);
} elseif ($isCallable) {
$passed = $stringCallable ? $rule($value) : $rule($value, $data, $error, $field);
} else {
$found = false;
// Check in our rulesets
foreach ($this->ruleSetInstances as $set) {
if (! method_exists($set, $rule)) {
continue;
}
$found = true;
if ($rule === 'field_exists') {
$passed = $set->{$rule}($value, $param, $data, $error, $originalField);
} else {
$passed = ($param === null)
? $set->{$rule}($value, $error)
: $set->{$rule}($value, $param, $data, $error, $field);
}
break;
}
// If the rule wasn't found anywhere, we
// should throw an exception so the developer can find it.
if (! $found) {
throw ValidationException::forRuleNotFound($rule);
}
}
// Set the error message if we didn't survive.
if ($passed === false) {
// if the $value is an array, convert it to as string representation
if (is_array($value)) {
$value = $this->isStringList($value)
? '[' . implode(', ', $value) . ']'
: json_encode($value);
} elseif (is_object($value)) {
$value = json_encode($value);
}
$fieldForErrors = ($rule === 'field_exists') ? $originalField : $field;
// @phpstan-ignore-next-line $error may be set by rule methods.
$this->errors[$fieldForErrors] = $error ?? $this->getErrorMessage(
($this->isClosure($rule) || $arrayCallable) ? (string) $i : $rule,
$field,
$label,
$param,
(string) $value,
$originalField,
);
return false;
}
}
return true;
}
/**
* @param array $data The array of data to validate, with `DBGroup`.
*
* @return array|true The modified rules or true if we return early
*/
private function processIfExist(string $field, array $rules, array $data)
{
if (in_array('if_exist', $rules, true)) {
$flattenedData = array_flatten_with_dots($data);
$ifExistField = $field;
if (str_contains($field, '.*')) {
// We'll change the dot notation into a PCRE pattern that can be used later
$ifExistField = str_replace('\.\*', '\.(?:[^\.]+)', preg_quote($field, '/'));
$dataIsExisting = false;
$pattern = sprintf('/%s/u', $ifExistField);
foreach (array_keys($flattenedData) as $item) {
if (preg_match($pattern, $item) === 1) {
$dataIsExisting = true;
break;
}
}
} elseif (str_contains($field, '.')) {
$dataIsExisting = array_key_exists($ifExistField, $flattenedData);
} else {
$dataIsExisting = array_key_exists($ifExistField, $data);
}
if (! $dataIsExisting) {
// we return early if `if_exist` is not satisfied. we have nothing to do here.
return true;
}
// Otherwise remove the if_exist rule and continue the process
$rules = array_filter($rules, static fn ($rule): bool => $rule instanceof Closure || $rule !== 'if_exist');
}
return $rules;
}
/**
* @param array|string $value
* @param array $data The array of data to validate, with `DBGroup`.
*
* @return array|true The modified rules or true if we return early
*/
private function processPermitEmpty($value, array $rules, array $data)
{
if (in_array('permit_empty', $rules, true)) {
if (
! in_array('required', $rules, true)
&& (is_array($value) ? $value === [] : trim((string) $value) === '')
) {
$passed = true;
foreach ($rules as $rule) {
if (! $this->isClosure($rule) && preg_match('/(.*?)\[(.*)\]/', $rule, $match)) {
$rule = $match[1];
$param = $match[2];
if (! in_array($rule, ['required_with', 'required_without'], true)) {
continue;
}
// Check in our rulesets
foreach ($this->ruleSetInstances as $set) {
if (! method_exists($set, $rule)) {
continue;
}
$passed = $passed && $set->{$rule}($value, $param, $data);
break;
}
}
}
if ($passed) {
return true;
}
}
$rules = array_filter($rules, static fn ($rule): bool => $rule instanceof Closure || $rule !== 'permit_empty');
}
return $rules;
}
/**
* @param Closure(bool|float|int|list<mixed>|object|string|null, bool|float|int|list<mixed>|object|string|null, string|null, string|null): (bool|string) $rule
*/
private function isClosure($rule): bool
{
return $rule instanceof Closure;
}
/**
* Is the array a string list `list<string>`?
*/
private function isStringList(array $array): bool
{
$expectedKey = 0;
foreach ($array as $key => $val) {
// Note: also covers PHP array key conversion, e.g. '5' and 5.1 both become 5
if (! is_int($key)) {
return false;
}
if ($key !== $expectedKey) {
return false;
}
$expectedKey++;
if (! is_string($val)) {
return false;
}
}
return true;
}
/**
* Takes a Request object and grabs the input data to use from its
* array values.
*/
public function withRequest(RequestInterface $request): ValidationInterface
{
/** @var IncomingRequest $request */
if (str_contains($request->getHeaderLine('Content-Type'), 'application/json')) {
$this->data = $request->getJSON(true);
if (! is_array($this->data)) {
throw HTTPException::forUnsupportedJSONFormat();
}
return $this;
}
if (in_array($request->getMethod(), [Method::PUT, Method::PATCH, Method::DELETE], true)
&& ! str_contains($request->getHeaderLine('Content-Type'), 'multipart/form-data')
) {
$this->data = $request->getRawInput();
} else {
$this->data = $request->getVar() ?? [];
}
return $this;
}
/**
* Sets (or adds) an individual rule and custom error messages for a single
* field.
*
* The custom error message should be just the messages that apply to
* this field, like so:
* [
* 'rule1' => 'message1',
* 'rule2' => 'message2',
* ]
*
* @param array|string $rules The validation rules.
* @param array $errors The custom error message.
*
* @return $this
*
* @throws InvalidArgumentException
*/
public function setRule(string $field, ?string $label, $rules, array $errors = [])
{
if (! is_array($rules) && ! is_string($rules)) {
throw new InvalidArgumentException('$rules must be of type string|array');
}
$ruleSet = [
$field => [
'label' => $label,
'rules' => $rules,
],
];
if ($errors !== []) {
$ruleSet[$field]['errors'] = $errors;
}
$this->setRules(array_merge($this->getRules(), $ruleSet), $this->customErrors);
return $this;
}
/**
* Stores the rules that should be used to validate the items.
*
* Rules should be an array formatted like:
* [
* 'field' => 'rule1|rule2'
* ]
*
* The $errors array should be formatted like:
* [
* 'field' => [
* 'rule1' => 'message1',
* 'rule2' => 'message2',
* ],
* ]
*
* @param array $errors An array of custom error messages
*/
public function setRules(array $rules, array $errors = []): ValidationInterface
{
$this->customErrors = $errors;
foreach ($rules as $field => &$rule) {
if (is_array($rule)) {
if (array_key_exists('errors', $rule)) {
$this->customErrors[$field] = $rule['errors'];
unset($rule['errors']);
}
// if $rule is already a rule collection, just move it to "rules"
// transforming [foo => [required, foobar]] to [foo => [rules => [required, foobar]]]
if (! array_key_exists('rules', $rule)) {
$rule = ['rules' => $rule];
}
}
if (isset($rule['rules']) && is_string($rule['rules'])) {
$rule['rules'] = $this->splitRules($rule['rules']);
}
if (is_string($rule)) {
$rule = ['rules' => $this->splitRules($rule)];
}
}
$this->rules = $rules;
return $this;
}
/**
* Returns all of the rules currently defined.
*/
public function getRules(): array
{
return $this->rules;
}
/**
* Checks to see if the rule for key $field has been set or not.
*/
public function hasRule(string $field): bool
{
return array_key_exists($field, $this->rules);
}
/**
* Get rule group.
*
* @param string $group Group.
*
* @return list<string> Rule group.
*
* @throws ValidationException If group not found.
*/
public function getRuleGroup(string $group): array
{
if (! isset($this->config->{$group})) {
throw ValidationException::forGroupNotFound($group);
}
if (! is_array($this->config->{$group})) {
throw ValidationException::forGroupNotArray($group);
}
return $this->config->{$group};
}
/**
* Set rule group.
*
* @param string $group Group.
*
* @return void
*
* @throws ValidationException If group not found.
*/
public function setRuleGroup(string $group)
{
$rules = $this->getRuleGroup($group);
$this->setRules($rules);
$errorName = $group . '_errors';
if (isset($this->config->{$errorName})) {
$this->customErrors = $this->config->{$errorName};
}
}
/**
* Returns the rendered HTML of the errors as defined in $template.
*
* You can also use validation_list_errors() in Form helper.
*/
public function listErrors(string $template = 'list'): string
{
if (! array_key_exists($template, $this->config->templates)) {
throw ValidationException::forInvalidTemplate($template);
}
return $this->view
->setVar('errors', $this->getErrors())
->render($this->config->templates[$template]);
}
/**
* Displays a single error in formatted HTML as defined in the $template view.
*
* You can also use validation_show_error() in Form helper.
*/
public function showError(string $field, string $template = 'single'): string
{
if (! array_key_exists($field, $this->getErrors())) {
return '';
}
if (! array_key_exists($template, $this->config->templates)) {
throw ValidationException::forInvalidTemplate($template);
}
return $this->view
->setVar('error', $this->getError($field))
->render($this->config->templates[$template]);
}
/**
* Loads all of the rulesets classes that have been defined in the
* Config\Validation and stores them locally so we can use them.
*
* @return void
*/
protected function loadRuleSets()
{
if ($this->ruleSetFiles === [] || $this->ruleSetFiles === null) {
throw ValidationException::forNoRuleSets();
}
foreach ($this->ruleSetFiles as $file) {
$this->ruleSetInstances[] = new $file();
}
}
/**
* Loads custom rule groups (if set) into the current rules.
*
* Rules can be pre-defined in Config\Validation and can
* be any name, but must all still be an array of the
* same format used with setRules(). Additionally, check
* for {group}_errors for an array of custom error messages.
*
* @param non-empty-string|null $group
*
* @return array<int, array> [rules, customErrors]
*
* @throws ValidationException
*/
public function loadRuleGroup(?string $group = null)
{
if ($group === null || $group === '') {
return [];
}
if (! isset($this->config->{$group})) {
throw ValidationException::forGroupNotFound($group);
}
if (! is_array($this->config->{$group})) {
throw ValidationException::forGroupNotArray($group);
}
$this->setRules($this->config->{$group});
// If {group}_errors exists in the config file,
// then override our custom errors with them.
$errorName = $group . '_errors';
if (isset($this->config->{$errorName})) {
$this->customErrors = $this->config->{$errorName};
}
return [$this->rules, $this->customErrors];
}
/**
* Replace any placeholders within the rules with the values that
* match the 'key' of any properties being set. For example, if
* we had the following $data array:
*
* [ 'id' => 13 ]
*
* and the following rule:
*
* 'is_unique[users,email,id,{id}]'
*
* The value of {id} would be replaced with the actual id in the form data:
*
* 'is_unique[users,email,id,13]'
*/
protected function fillPlaceholders(array $rules, array $data): array
{
foreach ($rules as &$rule) {
$ruleSet = $rule['rules'];
foreach ($ruleSet as &$row) {
if (is_string($row)) {
$placeholderFields = $this->retrievePlaceholders($row, $data);
foreach ($placeholderFields as $field) {
$validator ??= service('validation', null, false);
assert($validator instanceof Validation);
$placeholderRules = $rules[$field]['rules'] ?? null;
// Check if the validation rule for the placeholder exists
if ($placeholderRules === null) {
throw new LogicException(
'No validation rules for the placeholder: "' . $field
. '". You must set the validation rules for the field.'
. ' See <https://codeigniter4.github.io/userguide/libraries/validation.html#validation-placeholders>.',
);
}
// Check if the rule does not have placeholders
foreach ($placeholderRules as $placeholderRule) {
if ($this->retrievePlaceholders($placeholderRule, $data) !== []) {
throw new LogicException(
'The placeholder field cannot use placeholder: ' . $field,
);
}
}
// Validate the placeholder field
$dbGroup = $data['DBGroup'] ?? null;
if (! $validator->check($data[$field], $placeholderRules, [], $dbGroup)) {
// if fails, do nothing
continue;
}
// Replace the placeholder in the rule
$ruleSet = str_replace('{' . $field . '}', (string) $data[$field], $ruleSet);
}
}
}
$rule['rules'] = $ruleSet;
}
return $rules;
}
/**
* Retrieves valid placeholder fields.
*/
private function retrievePlaceholders(string $rule, array $data): array
{
preg_match_all('/{(.+?)}/', $rule, $matches);
return array_intersect($matches[1], array_keys($data));
}
/**
* Checks to see if an error exists for the given field.
*/
public function hasError(string $field): bool
{
return (bool) preg_grep(self::getRegex($field), array_keys($this->getErrors()));
}
/**
* Returns the error(s) for a specified $field (or empty string if not
* set).
*/
public function getError(?string $field = null): string
{
if ($field === null && count($this->rules) === 1) {
$field = array_key_first($this->rules);
}
$errors = array_filter(
$this->getErrors(),
static fn ($key): bool => preg_match(self::getRegex($field), $key) === 1,
ARRAY_FILTER_USE_KEY,
);
return $errors === [] ? '' : implode("\n", $errors);
}
/**
* Returns the array of errors that were encountered during
* a run() call. The array should be in the following format:
*
* [
* 'field1' => 'error message',
* 'field2' => 'error message',
* ]
*
* @return array<string, string>
*
* @codeCoverageIgnore
*/
public function getErrors(): array
{
return $this->errors;
}
/**
* Sets the error for a specific field. Used by custom validation methods.
*/
public function setError(string $field, string $error): ValidationInterface
{
$this->errors[$field] = $error;
return $this;
}
/**
* Attempts to find the appropriate error message
*
* @param non-empty-string|null $label
* @param string|null $value The value that caused the validation to fail.
*/
protected function getErrorMessage(
string $rule,
string $field,
?string $label = null,
?string $param = null,
?string $value = null,
?string $originalField = null,
): string {
$param ??= '';
$args = [
'field' => ($label === null || $label === '') ? $field : lang($label),
'param' => (! isset($this->rules[$param]['label'])) ? $param : lang($this->rules[$param]['label']),
'value' => $value ?? '',
];
// Check if custom message has been defined by user
if (isset($this->customErrors[$field][$rule])) {
return lang($this->customErrors[$field][$rule], $args);
}
if (null !== $originalField && isset($this->customErrors[$originalField][$rule])) {
return lang($this->customErrors[$originalField][$rule], $args);
}
// Try to grab a localized version of the message...
// lang() will return the rule name back if not found,
// so there will always be a string being returned.
return lang('Validation.' . $rule, $args);
}
/**
* Split rules string by pipe operator.
*/
protected function splitRules(string $rules): array
{
if (! str_contains($rules, '|')) {
return [$rules];
}
$string = $rules;
$rules = [];
$length = strlen($string);
$cursor = 0;
while ($cursor < $length) {
$pos = strpos($string, '|', $cursor);
if ($pos === false) {
// we're in the last rule
$pos = $length;
}
$rule = substr($string, $cursor, $pos - $cursor);
while (
(substr_count($rule, '[') - substr_count($rule, '\['))
!== (substr_count($rule, ']') - substr_count($rule, '\]'))
) {
// the pipe is inside the brackets causing the closing bracket to
// not be included. so, we adjust the rule to include that portion.
$pos = strpos($string, '|', $cursor + strlen($rule) + 1) ?: $length;
$rule = substr($string, $cursor, $pos - $cursor);
}
$rules[] = $rule;
$cursor += strlen($rule) + 1; // +1 to exclude the pipe
}
return array_unique($rules);
}
/**
* Resets the class to a blank slate. Should be called whenever
* you need to process more than one array.
*/
public function reset(): ValidationInterface
{
$this->data = [];
$this->validated = [];
$this->rules = [];
$this->errors = [];
$this->customErrors = [];
return $this;
}
}
@@ -0,0 +1,165 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Validation;
use CodeIgniter\Database\BaseConnection;
use CodeIgniter\HTTP\RequestInterface;
/**
* Expected behavior of a validator
*/
interface ValidationInterface
{
/**
* Runs the validation process, returning true/false determining whether
* validation was successful or not.
*
* @param array|null $data The array of data to validate.
* @param string|null $group The predefined group of rules to apply.
* @param array|BaseConnection|non-empty-string|null $dbGroup The database group to use.
*/
public function run(?array $data = null, ?string $group = null, $dbGroup = null): bool;
/**
* Check; runs the validation process, returning true or false
* determining whether or not validation was successful.
*
* @param array|bool|float|int|object|string|null $value Value to validate.
* @param array|string $rules
* @param list<string> $errors
* @param string|null $dbGroup The database group to use.
*
* @return bool True if valid, else false.
*/
public function check($value, $rules, array $errors = [], $dbGroup = null): bool;
/**
* Takes a Request object and grabs the input data to use from its
* array values.
*/
public function withRequest(RequestInterface $request): ValidationInterface;
/**
* Sets an individual rule and custom error messages for a single field.
*
* The custom error message should be just the messages that apply to
* this field, like so:
*
* [
* 'rule' => 'message',
* 'rule' => 'message',
* ]
*
* @param array|string $rules
*
* @return $this
*/
public function setRule(string $field, ?string $label, $rules, array $errors = []);
/**
* Stores the rules that should be used to validate the items.
*/
public function setRules(array $rules, array $messages = []): ValidationInterface;
/**
* Returns all of the rules currently defined.
*/
public function getRules(): array;
/**
* Checks to see if the rule for key $field has been set or not.
*/
public function hasRule(string $field): bool;
/**
* Get rule group.
*
* @param string $group Group.
*
* @return list<string> Rule group.
*/
public function getRuleGroup(string $group): array;
/**
* Set rule group.
*
* @param string $group Group.
*
* @return void
*/
public function setRuleGroup(string $group);
/**
* Returns the error for a specified $field (or empty string if not set).
*/
public function getError(string $field): string;
/**
* Returns the array of errors that were encountered during
* a run() call. The array should be in the following format:
*
* [
* 'field1' => 'error message',
* 'field2' => 'error message',
* ]
*
* @return array<string,string>
*/
public function getErrors(): array;
/**
* Sets the error for a specific field. Used by custom validation methods.
*/
public function setError(string $alias, string $error): ValidationInterface;
/**
* Resets the class to a blank slate. Should be called whenever
* you need to process more than one array.
*/
public function reset(): ValidationInterface;
/**
* Loads custom rule groups (if set) into the current rules.
*
* Rules can be pre-defined in Config\Validation and can
* be any name, but must all still be an array of the
* same format used with setRules(). Additionally, check
* for {group}_errors for an array of custom error messages.
*
* @param non-empty-string|null $group
*
* @return array
*/
public function loadRuleGroup(?string $group = null);
/**
* Checks to see if an error exists for the given field.
*/
public function hasError(string $field): bool;
/**
* Returns the rendered HTML of the errors as defined in $template.
*/
public function listErrors(string $template = 'list'): string;
/**
* Displays a single error in formatted HTML as defined in the $template view.
*/
public function showError(string $field, string $template = 'single'): string;
/**
* Returns the actual validated data.
*/
public function getValidated(): array;
}
@@ -0,0 +1,9 @@
<?php if (isset($errors) && $errors !== []) : ?>
<div class="errors" role="alert">
<ul>
<?php foreach ($errors as $error) : ?>
<li><?= esc($error) ?></li>
<?php endforeach ?>
</ul>
</div>
<?php endif ?>
@@ -0,0 +1 @@
<span class="help-block"><?= esc($error) ?></span>