24 private array $errorMessages = [];
27 private array $errors = [];
30 private array $lastResult = [];
33 private string $language =
'';
35 private static array $ruleCache = [];
38 private static array $messageCache = [];
41 private const DEFAULT_MESSAGES = [
43 'required' =>
'Bitte füllen Sie dieses Feld aus.',
44 'min_length' =>
'Die Eingabe ist zu kurz. Bitte ergänzen Sie den Wert.',
45 'max_length' =>
'Die Eingabe ist zu lang. Bitte kürzen Sie den Wert.',
46 'invalid_type' =>
'Bitte prüfen Sie Ihre Eingabe.',
47 'invalid_format' =>
'Bitte geben Sie einen gültigen Wert ein.',
48 'invalid_range' =>
'Der eingegebene Wert ist nicht zulässig.',
49 'invalid_rule' =>
'Das Formular konnte nicht geprüft werden. Bitte versuchen Sie es erneut.',
50 'invalid_array' =>
'Bitte prüfen Sie Ihre Auswahl.',
51 'invalid_array_item' =>
'Bitte prüfen Sie die ausgewählten Einträge.',
59 private const INT_RANGE = [
61 'tinyint' => [-128,127],
62 'smallint' => [-32768,32767],
63 'mediumint' => [-8388608,8388607],
64 'int' => [-2147483648,2147483647]
70 private const TEXT_LIMIT = [
74 'mediumtext' => 16777215,
75 'longtext' => 4294967295
79 private const BLOB_PASS = [
93 private const TYPE_ALIAS = [
100 'varchar2'=>
'varchar',
110 private const REGEX = [
112 'parameter' =>
'/^[a-zA-Z0-9._\-|]+$/',
113 'parameters' =>
'/^[a-zA-Z0-9._\/&=\-|]+$/',
117 'permalink' =>
'/^[a-z0-9]+(?:-[a-z0-9]+)*$/',
118 'datetime' =>
'/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d{1,6})?$/',
120 'word' =>
'/^[\p{L}\p{M}\p{N}@._,:;-]+$/u',
121 'words' =>
'/^[\p{L}\p{M}\p{N}\s@._,:;-]+$/u',
122 'sqlsearch' =>
'/^[\p{L}\p{M}\p{N}\s@._,:;\/\-]+$/u',
124 'alphanum' =>
'/^[\p{L}\p{M}0-9@_ .,:-]+$/u',
126 'phone' =>
'/^\+?[0-9\/\- ]+$/'
135 private function parseRule(
string $rules): array {
137 if (isset(self::$ruleCache[
$rules])) {
138 return self::$ruleCache[
$rules];
152 foreach(explode(
'|',
$rules) as $p){
159 if ($p ===
'array') {
164 if ($p ===
'required') {
165 $r[
'required'] =
true;
174 if (strpos($p,
'=')!==
false){
176 [$k,$v]=explode(
'=',$p,2);
178 if (($k ===
'min' || $k ===
'max') && preg_match(
'/^\d+$/', $v)) {
181 $r[
'invalid'][] = $p;
190 if (($pos=strpos($p,
'+'))!==
false){
191 $base = substr($p, 0, $pos);
192 $extra = substr($p, $pos + 1);
195 if ($r[
'base'] !==
'') {
200 if ($r[
'base'] ===
'*' &&
$base !==
'*') {
202 $r[
'extra'] = $extra;
209 $r[
'invalid'][] = $p;
214 $r[
'extra'] = $extra;
217 $r[
'base']=strtolower($r[
'base']);
219 if (isset(self::TYPE_ALIAS[$r[
'base']])) {
220 $r[
'base']=self::TYPE_ALIAS[$r[
'base']];
223 if ($r[
'base'] ===
'' || !$this->isKnownBase($r[
'base'])) {
224 $r[
'invalid'][] = $r[
'base'] ===
'' ?
'(empty)' : $r[
'base'];
227 if ($r[
'min'] !==
null && $r[
'max'] !==
null && $r[
'min'] > $r[
'max']) {
228 $r[
'invalid'][] =
'min>max';
231 self::$ruleCache[
$rules]=$r;
236 private function isKnownBase(
string $base):
bool {
239 'int',
'tinyint',
'smallint',
'mediumint',
'bigint',
240 'decimal',
'float',
'date',
'time',
'datetime',
'timestamp',
241 'varchar',
'char',
'password',
'boolean',
'email',
'json',
'year'
243 || isset(self::TEXT_LIMIT[
$base])
244 || isset(self::BLOB_PASS[
$base])
245 || isset(self::REGEX[
$base]);
253 private function stringLength(
string $value):
int {
254 if (function_exists(
'mb_strlen')) {
255 return mb_strlen(
$value,
'UTF-8');
267 private function stripExtra(
string $v,
string $extra):
string {
269 if ($extra===
'')
return $v;
271 return str_replace(str_split($extra),
'',$v);
279 private function validateDate(
string $v):
bool {
281 if (!preg_match(
'/^\d{4}-\d{2}-\d{2}$/',$v))
return false;
283 [$y,$m,$d]=explode(
'-',$v);
285 return checkdate((
int)$m,(
int)$d,(
int)$y);
288 private function validateTime(
string $v):
bool {
290 return (
bool)preg_match(
'/^([01]\d|2[0-3]):[0-5]\d(:[0-5]\d)?$/',$v);
293 private function validateTimestamp(
string $v):
bool {
296 '/^(\d{4})-(\d{2})-(\d{2}) ((?:[01]\d|2[0-3])):([0-5]\d):([0-5]\d)(?:\.\d{1,6})?$/',
303 return checkdate((
int)$m[2],(
int)$m[3],(
int)$m[1]);
317 private function validateEmail(
string $v):
bool {
319 if ($v ===
'' || strlen($v) > 254 || substr_count($v,
'@') !== 1) {
323 [
$local, $domain] = explode(
'@', $v, 2);
325 if (
$local ===
'' || strlen(
$local) > 64 || $domain ===
'' || strlen($domain) > 253) {
329 if (
$local[0] ===
'.' || substr(
$local, -1) ===
'.' || strpos(
$local,
'..') !==
false) {
333 if (function_exists(
'idn_to_ascii') && preg_match(
'/[^\x20-\x7E]/', $domain)) {
334 $idnFlags = defined(
'IDNA_DEFAULT') ? IDNA_DEFAULT : 0;
335 $idnVariant = defined(
'INTL_IDNA_VARIANT_UTS46') ? INTL_IDNA_VARIANT_UTS46 : 0;
336 $asciiDomain = idn_to_ascii($domain, $idnFlags, $idnVariant);
337 if ($asciiDomain ===
false) {
340 $domain = $asciiDomain;
343 if (strpos($domain,
'.') ===
false || strpos($domain,
'..') !==
false) {
347 $labels = explode(
'.', $domain);
348 foreach ($labels as $label) {
349 if ($label ===
'' || strlen($label) > 63
350 || !preg_match(
'/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i', $label)) {
355 $tld = (string)end($labels);
356 if (!preg_match(
'/^(?:[a-z]{2,63}|xn--[a-z0-9-]{2,59})$/i', $tld)) {
360 return filter_var(
$local .
'@' . $domain, FILTER_VALIDATE_EMAIL) !==
false;
368 private function validateInt(
string $v,
string $type):
bool {
370 if (!preg_match(
'/^-?\d+$/',$v))
return false;
372 if ($type ===
'bigint') {
373 $negative = str_starts_with($v,
'-');
374 $digits = ltrim($v,
'-0');
375 if ($digits ===
'') {
379 $limit = $negative ?
'9223372036854775808' :
'9223372036854775807';
380 return strlen($digits) < strlen($limit)
381 || (strlen($digits) === strlen($limit) && strcmp($digits, $limit) <= 0);
384 if (!isset(self::INT_RANGE[$type]))
return true;
386 [$min,$max]=self::INT_RANGE[$type];
390 return ($i>=$min && $i<=$max);
414 'message' => $this->message($code),
415 'details' => $details,
425 $this->language = $this->normalizeLanguage($language, true);
432 return $this->resolveLanguage();
435 private function normalizeLanguage(
string $language,
bool $allowEmpty =
false): string {
436 $language = strtolower(trim($language));
437 $language = preg_split(
'/[-_]/', $language, 2)[0] ??
'';
439 if ($language ===
'' && $allowEmpty) {
443 return preg_match(
'/^[a-z]{2,3}$/', $language) ? $language :
'de';
446 private function resolveLanguage(): string {
447 if ($this->language !==
'') {
448 return $this->language;
451 if (function_exists(
'dbx_lng_current')) {
454 }
catch (\Throwable $e) {
461 private function messages(
string $language): array {
462 $language = $this->normalizeLanguage($language);
463 if (isset(self::$messageCache[$language])) {
464 return self::$messageCache[$language];
468 $defaultFile = __DIR__ .
'/lang/dbxValidator_de.php';
469 if (is_file($defaultFile)) {
470 $loaded = include $defaultFile;
471 if (is_array($loaded)) {
476 if ($language !==
'de') {
477 $languageFile = __DIR__ .
'/lang/dbxValidator_' . $language .
'.php';
478 if (is_file($languageFile)) {
479 $loaded = include $languageFile;
480 if (is_array($loaded)) {
486 self::$messageCache[$language] =
$messages;
490 private function message(
string $code): string {
491 $messages = $this->messages($this->resolveLanguage());
495 private function validateScalarResult(
$value, array
$rule,
string $rules,
string $name): array {
497 $extra =
$rule[
'extra'];
505 'actual_type' =>
'boolean',
509 } elseif (
$value ===
null) {
523 $length = $this->stringLength(
$value);
524 if ($min !==
null && $length < $min) {
530 if ($max !==
null && $length > $max) {
541 $checkValue = $this->stripExtra(
$value, $extra);
542 if ($checkValue ===
'') {
546 $code =
'invalid_format';
559 $ok=$this->validateInt($checkValue,
$base);
560 $code = preg_match(
'/^-?\d+$/', $checkValue) ?
'invalid_range' :
'invalid_format';
565 $ok=is_numeric($checkValue);
569 $ok=$this->validateDate($checkValue);
573 $ok=$this->validateTime($checkValue);
578 $ok=$this->validateTimestamp($checkValue);
588 $ok=($checkValue===
'0'||$checkValue===
'1'||$checkValue===
'true'||$checkValue===
'false');
592 $ok=$this->validateEmail($checkValue);
596 json_decode($checkValue);
597 $ok=(json_last_error()===JSON_ERROR_NONE);
601 $ok=preg_match(
'/^\d{4}$/',$checkValue);
606 if (isset(self::TEXT_LIMIT[
$base])){
607 $ok=$this->stringLength($checkValue)<=self::TEXT_LIMIT[
$base];
608 $code =
'max_length';
612 if (isset(self::BLOB_PASS[
$base])){
617 if (isset(self::REGEX[
$base])){
618 $ok=preg_match(self::REGEX[
$base],$checkValue);
662 $name = (string)$name;
667 return $this->recordResult(
674 if (
$rule[
'invalid']) {
675 return $this->recordResult($this->
result(
679 (
string)
$rule[
'base'],
682 [
'invalid' => array_values(array_unique(
$rule[
'invalid']))]
686 if (
$rule[
'array']) {
691 return $this->recordResult(
$result);
695 return $this->recordResult(
701 return $this->recordResult(
708 if (is_array($item) || is_object($item) || is_resource($item)
709 || (!is_scalar($item) && $item !==
null)) {
710 return $this->recordResult($this->
result(
716 'invalid_array_item',
717 [
'index' =>
$index,
'item_code' =>
'invalid_type']
721 $itemResult = $this->validateScalarResult($item,
$rule,
$rules, $name .
'[' .
$index .
']');
723 if (!$itemResult[
'valid']) {
724 return $this->recordResult($this->
result(
730 'invalid_array_item',
731 [
'index' =>
$index,
'item' => $itemResult]
736 return $this->recordResult(
743 return $this->recordResult($this->
result(
750 [
'actual_type' => gettype(
$value)]
754 return $this->recordResult(
793 $length = (int)$length;
795 if ($length>0 && strlen(
$value)>$length)
807 return $this->errorMessages;
814 return $this->errors;
821 return $this->lastResult;
825 $this->errorMessages=[];
827 $this->lastResult=[];
830 private function recordResult(array
$result): array {
832 if (!(
$result[
'valid'] ??
false)) {
834 $this->errorMessages[] = (string)(
$result[
'message'] ??
'');
843 if (!is_array(
$rules))
return '';
846 $r = trim((
string)$r);
849 && strpos($r,
'=') ===
false
850 && !in_array($r, array(
'array',
'required',
'trim'),
true))
853 if (strpos($r,
'=')!==
false){
855 [$k,$v]=explode(
'=',$r,2);