dbxapp 4.1.3
CMS, Shop, Workflows und modulare Geschäftsanwendungen
Loading...
Searching...
No Matches
dbxValidator.class.php
Go to the documentation of this file.
1<?php
2
23
24 private array $errorMessages = [];
25
27 private array $errors = [];
28
30 private array $lastResult = [];
31
33 private string $language = '';
34
35 private static array $ruleCache = [];
36
38 private static array $messageCache = [];
39
41 private const DEFAULT_MESSAGES = [
42 'ok' => '',
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.',
52 ];
53
54
55 /* =====================================================
56 DB TYPE LIMITS
57 ===================================================== */
58
59 private const INT_RANGE = [
60
61 'tinyint' => [-128,127],
62 'smallint' => [-32768,32767],
63 'mediumint' => [-8388608,8388607],
64 'int' => [-2147483648,2147483647]
65
66 // 🔥 bigint entfernt → wird separat geprüft
67
68 ];
69
70 private const TEXT_LIMIT = [
71
72 'tinytext' => 255,
73 'text' => 65535,
74 'mediumtext' => 16777215,
75 'longtext' => 4294967295
76
77 ];
78
79 private const BLOB_PASS = [
80
81 'blob'=>true,
82 'tinyblob'=>true,
83 'mediumblob'=>true,
84 'longblob'=>true
85
86 ];
87
88
89 /* =====================================================
90 TYPE ALIAS
91 ===================================================== */
92
93 private const TYPE_ALIAS = [
94
95 'integer'=>'int',
96 'numeric'=>'decimal',
97 'double'=>'float',
98 'real'=>'float',
99
100 'varchar2'=>'varchar',
101 'bool'=>'boolean'
102
103 ];
104
105
106 /* =====================================================
107 REGEX
108 ===================================================== */
109
110 private const REGEX = [
111
112 'parameter' => '/^[a-zA-Z0-9._\-|]+$/',
113 'parameters' => '/^[a-zA-Z0-9._\/&=\-|]+$/',
114 // CMS-Permalinks sind absichtlich flach und portabel. Ordnerpfade,
115 // Leerzeichen, Sonderzeichen sowie fuehrende/doppelte Bindestriche
116 // sind nicht erlaubt.
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})?$/',
119
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',
123
124 'alphanum' => '/^[\p{L}\p{M}0-9@_ .,:-]+$/u',
125
126 'phone' => '/^\+?[0-9\/\- ]+$/'
127
128 ];
129
130
131 /* =====================================================
132 RULE PARSER
133 ===================================================== */
134
135 private function parseRule(string $rules): array {
136
137 if (isset(self::$ruleCache[$rules])) {
138 return self::$ruleCache[$rules];
139 }
140
141 $r=[
142 'base' => '',
143 'extra' => '',
144 'min' => null,
145 'max' => null,
146 'array' => false,
147 'required' => false,
148 'trim' => false,
149 'invalid' => [],
150 ];
151
152 foreach(explode('|',$rules) as $p){
153 $p = trim($p);
154
155 if ($p === '') {
156 continue;
157 }
158
159 if ($p === 'array') {
160 $r['array'] = true;
161 continue;
162 }
163
164 if ($p === 'required') {
165 $r['required'] = true;
166 continue;
167 }
168
169 if ($p === 'trim') {
170 $r['trim'] = true;
171 continue;
172 }
173
174 if (strpos($p,'=')!==false){
175
176 [$k,$v]=explode('=',$p,2);
177
178 if (($k === 'min' || $k === 'max') && preg_match('/^\d+$/', $v)) {
179 $r[$k] = (int)$v;
180 } else {
181 $r['invalid'][] = $p;
182 }
183
184 continue;
185 }
186
187 $base = $p;
188 $extra = '';
189
190 if (($pos=strpos($p,'+'))!==false){
191 $base = substr($p, 0, $pos);
192 $extra = substr($p, $pos + 1);
193 }
194
195 if ($r['base'] !== '') {
196 // Historische Regeln verwenden `*|date` beziehungsweise
197 // `*|parameter`. Das fuehrende `*` bedeutet dabei nur, dass
198 // kein engerer Default vorgegeben ist; die konkrete Regel
199 // muss weiterhin ausgewertet werden.
200 if ($r['base'] === '*' && $base !== '*') {
201 $r['base'] = $base;
202 $r['extra'] = $extra;
203 continue;
204 }
205 if ($base === '*') {
206 continue;
207 }
208
209 $r['invalid'][] = $p;
210 continue;
211 }
212
213 $r['base'] = $base;
214 $r['extra'] = $extra;
215 }
216
217 $r['base']=strtolower($r['base']);
218
219 if (isset(self::TYPE_ALIAS[$r['base']])) {
220 $r['base']=self::TYPE_ALIAS[$r['base']];
221 }
222
223 if ($r['base'] === '' || !$this->isKnownBase($r['base'])) {
224 $r['invalid'][] = $r['base'] === '' ? '(empty)' : $r['base'];
225 }
226
227 if ($r['min'] !== null && $r['max'] !== null && $r['min'] > $r['max']) {
228 $r['invalid'][] = 'min>max';
229 }
230
231 self::$ruleCache[$rules]=$r;
232
233 return $r;
234 }
235
236 private function isKnownBase(string $base): bool {
237 return $base === '*'
238 || in_array($base, [
239 'int', 'tinyint', 'smallint', 'mediumint', 'bigint',
240 'decimal', 'float', 'date', 'time', 'datetime', 'timestamp',
241 'varchar', 'char', 'password', 'boolean', 'email', 'json', 'year'
242 ], true)
243 || isset(self::TEXT_LIMIT[$base])
244 || isset(self::BLOB_PASS[$base])
245 || isset(self::REGEX[$base]);
246 }
247
248
249 /* =====================================================
250 LENGTH
251 ===================================================== */
252
253 private function stringLength(string $value): int {
254 if (function_exists('mb_strlen')) {
255 return mb_strlen($value, 'UTF-8');
256 }
257
258 $count = preg_match_all('/./us', $value, $matches);
259 return $count === false ? strlen($value) : $count;
260 }
261
262
263 /* =====================================================
264 EXTRA STRIP
265 ===================================================== */
266
267 private function stripExtra(string $v,string $extra): string {
268
269 if ($extra==='') return $v;
270
271 return str_replace(str_split($extra),'',$v);
272 }
273
274
275 /* =====================================================
276 DATE / TIME
277 ===================================================== */
278
279 private function validateDate(string $v): bool {
280
281 if (!preg_match('/^\d{4}-\d{2}-\d{2}$/',$v)) return false;
282
283 [$y,$m,$d]=explode('-',$v);
284
285 return checkdate((int)$m,(int)$d,(int)$y);
286 }
287
288 private function validateTime(string $v): bool {
289
290 return (bool)preg_match('/^([01]\d|2[0-3]):[0-5]\d(:[0-5]\d)?$/',$v);
291 }
292
293 private function validateTimestamp(string $v): bool {
294
295 if (!preg_match(
296 '/^(\d{4})-(\d{2})-(\d{2}) ((?:[01]\d|2[0-3])):([0-5]\d):([0-5]\d)(?:\.\d{1,6})?$/',
297 $v,
298 $m
299 )) {
300 return false;
301 }
302
303 return checkdate((int)$m[2],(int)$m[3],(int)$m[1]);
304 }
305
306
307 /* =====================================================
308 E-MAIL
309 ===================================================== */
310
317 private function validateEmail(string $v): bool {
318
319 if ($v === '' || strlen($v) > 254 || substr_count($v, '@') !== 1) {
320 return false;
321 }
322
323 [$local, $domain] = explode('@', $v, 2);
324
325 if ($local === '' || strlen($local) > 64 || $domain === '' || strlen($domain) > 253) {
326 return false;
327 }
328
329 if ($local[0] === '.' || substr($local, -1) === '.' || strpos($local, '..') !== false) {
330 return false;
331 }
332
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) {
338 return false;
339 }
340 $domain = $asciiDomain;
341 }
342
343 if (strpos($domain, '.') === false || strpos($domain, '..') !== false) {
344 return false;
345 }
346
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)) {
351 return false;
352 }
353 }
354
355 $tld = (string)end($labels);
356 if (!preg_match('/^(?:[a-z]{2,63}|xn--[a-z0-9-]{2,59})$/i', $tld)) {
357 return false;
358 }
359
360 return filter_var($local . '@' . $domain, FILTER_VALIDATE_EMAIL) !== false;
361 }
362
363
364 /* =====================================================
365 INT VALIDATION
366 ===================================================== */
367
368 private function validateInt(string $v,string $type): bool {
369
370 if (!preg_match('/^-?\d+$/',$v)) return false;
371
372 if ($type === 'bigint') {
373 $negative = str_starts_with($v, '-');
374 $digits = ltrim($v, '-0');
375 if ($digits === '') {
376 return true;
377 }
378
379 $limit = $negative ? '9223372036854775808' : '9223372036854775807';
380 return strlen($digits) < strlen($limit)
381 || (strlen($digits) === strlen($limit) && strcmp($digits, $limit) <= 0);
382 }
383
384 if (!isset(self::INT_RANGE[$type])) return true;
385
386 [$min,$max]=self::INT_RANGE[$type];
387
388 $i=(int)$v;
389
390 return ($i>=$min && $i<=$max);
391 }
392
393
394 /* =====================================================
395 SCALAR VALIDATION
396 ===================================================== */
397
398 private function result(
399 bool $valid,
400 string $name,
401 string $rules,
402 string $base,
404 string $code = 'ok',
405 array $details = []
406 ): array {
407 return [
408 'valid' => $valid,
409 'name' => $name,
410 'rules' => $rules,
411 'rule' => $base,
412 'normalized' => $normalized,
413 'code' => $code,
414 'message' => $this->message($code),
415 'details' => $details,
416 ];
417 }
418
424 public function setLanguage(string $language = ''): void {
425 $this->language = $this->normalizeLanguage($language, true);
426 }
427
431 public function getLanguage(): string {
432 return $this->resolveLanguage();
433 }
434
435 private function normalizeLanguage(string $language, bool $allowEmpty = false): string {
436 $language = strtolower(trim($language));
437 $language = preg_split('/[-_]/', $language, 2)[0] ?? '';
438
439 if ($language === '' && $allowEmpty) {
440 return '';
441 }
442
443 return preg_match('/^[a-z]{2,3}$/', $language) ? $language : 'de';
444 }
445
446 private function resolveLanguage(): string {
447 if ($this->language !== '') {
448 return $this->language;
449 }
450
451 if (function_exists('dbx_lng_current')) {
452 try {
453 return $this->normalizeLanguage((string)dbx_lng_current());
454 } catch (\Throwable $e) {
455 }
456 }
457
458 return 'de';
459 }
460
461 private function messages(string $language): array {
462 $language = $this->normalizeLanguage($language);
463 if (isset(self::$messageCache[$language])) {
464 return self::$messageCache[$language];
465 }
466
467 $messages = self::DEFAULT_MESSAGES;
468 $defaultFile = __DIR__ . '/lang/dbxValidator_de.php';
469 if (is_file($defaultFile)) {
470 $loaded = include $defaultFile;
471 if (is_array($loaded)) {
472 $messages = array_merge($messages, $loaded);
473 }
474 }
475
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)) {
481 $messages = array_merge($messages, $loaded);
482 }
483 }
484 }
485
486 self::$messageCache[$language] = $messages;
487 return $messages;
488 }
489
490 private function message(string $code): string {
491 $messages = $this->messages($this->resolveLanguage());
492 return (string)($messages[$code] ?? $messages['invalid_format'] ?? '');
493 }
494
495 private function validateScalarResult($value, array $rule, string $rules, string $name): array {
496 $base = $rule['base'];
497 $extra = $rule['extra'];
498 $min = $rule['min'];
499 $max = $rule['max'];
500 $required = (bool)$rule['required'];
501
502 if (is_bool($value)) {
503 if ($base !== 'boolean' && $base !== '*') {
504 return $this->result(false, $name, $rules, $base, $value, 'invalid_type', [
505 'actual_type' => 'boolean',
506 ]);
507 }
508 $value = $value ? '1' : '0';
509 } elseif ($value === null) {
510 $value = '';
511 } else {
512 $value = (string)$value;
513 }
514
515 if ($rule['trim']) {
516 $value = trim($value);
517 }
518
519 if ($required && $value === '') {
520 return $this->result(false, $name, $rules, $base, $value, 'required');
521 }
522
523 $length = $this->stringLength($value);
524 if ($min !== null && $length < $min) {
525 return $this->result(false, $name, $rules, $base, $value, 'min_length', [
526 'min' => $min,
527 'actual' => $length,
528 ]);
529 }
530 if ($max !== null && $length > $max) {
531 return $this->result(false, $name, $rules, $base, $value, 'max_length', [
532 'max' => $max,
533 'actual' => $length,
534 ]);
535 }
536
537 if ($value === '') {
538 return $this->result(true, $name, $rules, $base, $value);
539 }
540
541 $checkValue = $this->stripExtra($value, $extra);
542 if ($checkValue === '') {
543 return $this->result(true, $name, $rules, $base, $value);
544 }
545
546 $code = 'invalid_format';
547
548 switch($base){
549
550 case '*':
551 $ok=true;
552 break;
553
554 case 'int':
555 case 'tinyint':
556 case 'smallint':
557 case 'mediumint':
558 case 'bigint':
559 $ok=$this->validateInt($checkValue,$base);
560 $code = preg_match('/^-?\d+$/', $checkValue) ? 'invalid_range' : 'invalid_format';
561 break;
562
563 case 'decimal':
564 case 'float':
565 $ok=is_numeric($checkValue);
566 break;
567
568 case 'date':
569 $ok=$this->validateDate($checkValue);
570 break;
571
572 case 'time':
573 $ok=$this->validateTime($checkValue);
574 break;
575
576 case 'datetime':
577 case 'timestamp':
578 $ok=$this->validateTimestamp($checkValue);
579 break;
580
581 case 'varchar':
582 case 'char':
583 case 'password':
584 $ok=true;
585 break;
586
587 case 'boolean':
588 $ok=($checkValue==='0'||$checkValue==='1'||$checkValue==='true'||$checkValue==='false');
589 break;
590
591 case 'email':
592 $ok=$this->validateEmail($checkValue);
593 break;
594
595 case 'json':
596 json_decode($checkValue);
597 $ok=(json_last_error()===JSON_ERROR_NONE);
598 break;
599
600 case 'year':
601 $ok=preg_match('/^\d{4}$/',$checkValue);
602 break;
603
604 default:
605
606 if (isset(self::TEXT_LIMIT[$base])){
607 $ok=$this->stringLength($checkValue)<=self::TEXT_LIMIT[$base];
608 $code = 'max_length';
609 break;
610 }
611
612 if (isset(self::BLOB_PASS[$base])){
613 $ok=true;
614 break;
615 }
616
617 if (isset(self::REGEX[$base])){
618 $ok=preg_match(self::REGEX[$base],$checkValue);
619 break;
620 }
621
622 return $this->result(false, $name, $rules, $base, $value, 'invalid_rule');
623 }
624
625 if (!$ok) {
626 return $this->result(false, $name, $rules, $base, $value, $code);
627 }
628
629 return $this->result(true, $name, $rules, $base, $value);
630 }
631
632
633 /* =====================================================
634 PUBLIC VALIDATE (STRICT)
635 ===================================================== */
636
658 public function validateResult($value, $rules = 'parameter', $name = '-undef-'): array {
659 $this->clearErrors();
660
661 $rules = trim((string)$rules);
662 $name = (string)$name;
663
664 // Historischer Vollpass: `*` akzeptiert bewusst auch Arrays und
665 // andere Werte. Engere Arraypruefung wird mit `array|...` aktiviert.
666 if ($rules === '*') {
667 return $this->recordResult(
668 $this->result(true, $name, $rules, '*', $value)
669 );
670 }
671
672 $rule = $this->parseRule($rules);
673
674 if ($rule['invalid']) {
675 return $this->recordResult($this->result(
676 false,
677 $name,
678 $rules,
679 (string)$rule['base'],
680 $value,
681 'invalid_rule',
682 ['invalid' => array_values(array_unique($rule['invalid']))]
683 ));
684 }
685
686 if ($rule['array']) {
687 if ($value === null || $value === '') {
688 $result = $rule['required']
689 ? $this->result(false, $name, $rules, $rule['base'], [], 'required')
690 : $this->result(true, $name, $rules, $rule['base'], []);
691 return $this->recordResult($result);
692 }
693
694 if (!is_array($value)) {
695 return $this->recordResult(
696 $this->result(false, $name, $rules, $rule['base'], $value, 'invalid_array')
697 );
698 }
699
700 if ($rule['required'] && count($value) === 0) {
701 return $this->recordResult(
702 $this->result(false, $name, $rules, $rule['base'], [], 'required')
703 );
704 }
705
706 $normalized = [];
707 foreach ($value as $index => $item) {
708 if (is_array($item) || is_object($item) || is_resource($item)
709 || (!is_scalar($item) && $item !== null)) {
710 return $this->recordResult($this->result(
711 false,
712 $name,
713 $rules,
714 $rule['base'],
716 'invalid_array_item',
717 ['index' => $index, 'item_code' => 'invalid_type']
718 ));
719 }
720
721 $itemResult = $this->validateScalarResult($item, $rule, $rules, $name . '[' . $index . ']');
722 $normalized[$index] = $itemResult['normalized'];
723 if (!$itemResult['valid']) {
724 return $this->recordResult($this->result(
725 false,
726 $name,
727 $rules,
728 $rule['base'],
730 'invalid_array_item',
731 ['index' => $index, 'item' => $itemResult]
732 ));
733 }
734 }
735
736 return $this->recordResult(
737 $this->result(true, $name, $rules, $rule['base'], $normalized)
738 );
739 }
740
741 if (is_array($value) || is_object($value) || is_resource($value)
742 || (!is_scalar($value) && $value !== null)) {
743 return $this->recordResult($this->result(
744 false,
745 $name,
746 $rules,
747 $rule['base'],
748 $value,
749 'invalid_type',
750 ['actual_type' => gettype($value)]
751 ));
752 }
753
754 return $this->recordResult(
755 $this->validateScalarResult($value, $rule, $rules, $name)
756 );
757 }
758
764 public function validate($value,$rules='parameter',$name='-undef-'): bool {
765 $result = $this->validateResult($value, $rules, $name);
766 return (bool)$result['valid'];
767 }
768
769
770 /* =====================================================
771 CLEAN
772 ===================================================== */
773
787 public function clean($value,$rules,$length=-1,$name='-undef-'){
788
789 if (!is_scalar($value) && $value !== null) return '';
790
791 if ($value===null) $value='';
792 $value = (string)$value;
793 $length = (int)$length;
794
795 if ($length>0 && strlen($value)>$length)
796 $value=substr($value,0,$length);
797
798 return $value;
799 }
800
801
802 /* =====================================================
803 UTIL
804 ===================================================== */
805
806 public function getErrorMessages(): array {
807 return $this->errorMessages;
808 }
809
813 public function getErrors(): array {
814 return $this->errors;
815 }
816
820 public function getLastResult(): array {
821 return $this->lastResult;
822 }
823
824 public function clearErrors(): void {
825 $this->errorMessages=[];
826 $this->errors=[];
827 $this->lastResult=[];
828 }
829
830 private function recordResult(array $result): array {
831 $this->lastResult = $result;
832 if (!($result['valid'] ?? false)) {
833 $this->errors[] = $result;
834 $this->errorMessages[] = (string)($result['message'] ?? '');
835 }
836 return $result;
837 }
838
839 public function get_rule_val($rules,$rule=''){
840
841 if (is_string($rules)) $rules=explode('|',$rules);
842
843 if (!is_array($rules)) return '';
844
845 foreach ($rules as $r){
846 $r = trim((string)$r);
847
848 if ($rule === 'rule'
849 && strpos($r, '=') === false
850 && !in_array($r, array('array', 'required', 'trim'), true))
851 return $r;
852
853 if (strpos($r,'=')!==false){
854
855 [$k,$v]=explode('=',$r,2);
856
857 if ($k===$rule)
858 return $v;
859 }
860 }
861
862 return '';
863 }
864
865}
Zentrale, strikt pruefende Eingabevalidierung.
validate($value, $rules='parameter', $name='-undef-')
Rueckwaertskompatibler boolescher Einstieg.
clean($value, $rules, $length=-1, $name='-undef-')
Historische Fehlerbehandlung fuer den DB-Clean-Modus.
getLanguage()
Liefert die aktuell wirksame Sprache.
setLanguage(string $language='')
Setzt die Sprache für folgende Validierungen explizit.
get_rule_val($rules, $rule='')
getLastResult()
Liefert das vollstaendige Ergebnis des letzten Validierungslaufs.
getErrors()
Liefert die strukturierten Fehler des letzten Validierungslaufs.
validateResult($value, $rules='parameter', $name='-undef-')
Validiert einen Wert und liefert ein maschinenlesbares Ergebnis.
$messages
Definition config.fd.php:2
dbx_lng_current()
Lädt eine Klasse aus dem Cache oder erstellt eine neue Instanz der Klasse.
Definition dbxApi.php:3038
if(!defined( 'IMG_WEBP')) define( 'IMG_WEBP'