1<?php 2/* 3** Zabbix 4** Copyright (C) 2001-2021 Zabbix SIA 5** 6** This program is free software; you can redistribute it and/or modify 7** it under the terms of the GNU General Public License as published by 8** the Free Software Foundation; either version 2 of the License, or 9** (at your option) any later version. 10** 11** This program is distributed in the hope that it will be useful, 12** but WITHOUT ANY WARRANTY; without even the implied warranty of 13** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14** GNU General Public License for more details. 15** 16** You should have received a copy of the GNU General Public License 17** along with this program; if not, write to the Free Software 18** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19**/ 20 21 22class CNewValidator { 23 24 private $rules; 25 private $input = []; 26 private $output = []; 27 private $errors = []; 28 private $errorsFatal = []; 29 30 /** 31 * Parser for validation rules. 32 * 33 * @var CValidationRule 34 */ 35 private $validationRuleParser; 36 37 /** 38 * Parser for range date/time. 39 * 40 * @var CRangeTimeParser 41 */ 42 private $range_time_parser; 43 44 /** 45 * A parser for a list of time periods separated by a semicolon. 46 * 47 * @var CTimePeriodsParser 48 */ 49 private $time_periods_parser; 50 51 public function __construct(array $input, array $rules) { 52 $this->input = $input; 53 $this->rules = $rules; 54 $this->validationRuleParser = new CValidationRule(); 55 56 $this->validate(); 57 } 58 59 /** 60 * Returns true if the given $value is valid, or set's an error and returns false otherwise. 61 */ 62 private function validate() { 63 foreach ($this->rules as $field => $rule) { 64 $result = $this->validateField($field, $rule); 65 66 if (array_key_exists($field, $this->input)) { 67 $this->output[$field] = $this->input[$field]; 68 } 69 } 70 } 71 72 private function validateField($field, $rules) { 73 if (false === ($rules = $this->validationRuleParser->parse($rules))) { 74 $this->addError(true, $this->validationRuleParser->getError()); 75 return false; 76 } 77 78 $fatal = array_key_exists('fatal', $rules); 79 80 $flags = array_key_exists('flags', $rules) ? $rules['flags'] : 0x00; 81 82 foreach ($rules as $rule => $params) { 83 switch ($rule) { 84 /* 85 * 'fatal' => true 86 */ 87 case 'fatal': 88 // nothing to do 89 break; 90 91 /* 92 * 'not_empty' => true 93 */ 94 case 'not_empty': 95 if (array_key_exists($field, $this->input) && $this->input[$field] === '') { 96 $this->addError($fatal, 97 _s('Incorrect value for field "%1$s": %2$s.', $field, _('cannot be empty')) 98 ); 99 return false; 100 } 101 break; 102 103 case 'json': 104 if (array_key_exists($field, $this->input)) { 105 if (!is_string($this->input[$field]) || json_decode($this->input[$field]) === null) { 106 $this->addError($fatal, 107 _s('Incorrect value for field "%1$s": %2$s.', $field, _('JSON string is expected')) 108 ); 109 return false; 110 } 111 } 112 break; 113 114 /* 115 * 'in' => array(<values>) 116 */ 117 case 'in': 118 if (array_key_exists($field, $this->input)) { 119 if (!is_string($this->input[$field]) || !in_array($this->input[$field], $params)) { 120 $this->addError($fatal, 121 is_scalar($this->input[$field]) 122 ? _s('Incorrect value "%1$s" for "%2$s" field.', $this->input[$field], $field) 123 : _s('Incorrect value for "%1$s" field.', $field) 124 ); 125 return false; 126 } 127 } 128 break; 129 130 case 'int32': 131 if (array_key_exists($field, $this->input)) { 132 if (!is_string($this->input[$field]) || !self::is_int32($this->input[$field])) { 133 $this->addError($fatal, 134 is_scalar($this->input[$field]) 135 ? _s('Incorrect value "%1$s" for "%2$s" field.', $this->input[$field], $field) 136 : _s('Incorrect value for "%1$s" field.', $field) 137 ); 138 return false; 139 } 140 } 141 break; 142 143 case 'id': 144 if (array_key_exists($field, $this->input)) { 145 if (!is_string($this->input[$field]) || !self::is_id($this->input[$field])) { 146 $this->addError($fatal, 147 is_scalar($this->input[$field]) 148 ? _s('Incorrect value "%1$s" for "%2$s" field.', $this->input[$field], $field) 149 : _s('Incorrect value for "%1$s" field.', $field) 150 ); 151 return false; 152 } 153 } 154 break; 155 156 /* 157 * 'array_id' => true 158 */ 159 case 'array_id': 160 if (array_key_exists($field, $this->input)) { 161 if (!is_array($this->input[$field]) || !$this->is_array_id($this->input[$field])) { 162 $this->addError($fatal, 163 is_scalar($this->input[$field]) 164 ? _s('Incorrect value "%1$s" for "%2$s" field.', $this->input[$field], $field) 165 : _s('Incorrect value for "%1$s" field.', $field) 166 ); 167 return false; 168 } 169 } 170 break; 171 172 /* 173 * 'array' => true 174 */ 175 case 'array': 176 if (array_key_exists($field, $this->input) && !is_array($this->input[$field])) { 177 $this->addError($fatal, 178 _s('Incorrect value "%1$s" for "%2$s" field.', $this->input[$field], $field) 179 ); 180 181 return false; 182 } 183 break; 184 185 /* 186 * 'array_db' => array( 187 * 'table' => <table_name>, 188 * 'field' => <field_name> 189 * ) 190 */ 191 case 'array_db': 192 if (array_key_exists($field, $this->input)) { 193 if (!is_array($this->input[$field]) 194 || !$this->is_array_db($this->input[$field], $params['table'], $params['field'], $flags) 195 ) { 196 $this->addError($fatal, 197 is_scalar($this->input[$field]) 198 ? _s('Incorrect value "%1$s" for "%2$s" field.', $this->input[$field], $field) 199 : _s('Incorrect value for "%1$s" field.', $field) 200 ); 201 return false; 202 } 203 } 204 break; 205 206 /* 207 * 'ge' => <value> 208 */ 209 case 'ge': 210 if (array_key_exists($field, $this->input)) { 211 if (!is_string($this->input[$field]) || !self::is_int32($this->input[$field]) 212 || $this->input[$field] < $params) { 213 $this->addError($fatal, 214 _s('Incorrect value "%1$s" for "%2$s" field.', $this->input[$field], $field) 215 ); 216 217 return false; 218 } 219 } 220 break; 221 222 /* 223 * 'le' => <value> 224 */ 225 case 'le': 226 if (array_key_exists($field, $this->input)) { 227 if (!is_string($this->input[$field]) || !self::is_int32($this->input[$field]) 228 || $this->input[$field] > $params) { 229 $this->addError($fatal, 230 _s('Incorrect value "%1$s" for "%2$s" field.', $this->input[$field], $field) 231 ); 232 233 return false; 234 } 235 } 236 break; 237 238 /* 239 * 'db' => array( 240 * 'table' => <table_name>, 241 * 'field' => <field_name> 242 * ) 243 */ 244 case 'db': 245 if (array_key_exists($field, $this->input)) { 246 if (!$this->is_db($this->input[$field], $params['table'], $params['field'], $flags)) { 247 $this->addError($fatal, 248 is_scalar($this->input[$field]) 249 ? _s('Incorrect value "%1$s" for "%2$s" field.', $this->input[$field], $field) 250 : _s('Incorrect value for "%1$s" field.', $field) 251 ); 252 return false; 253 } 254 } 255 break; 256 257 /* 258 * 'required' => true 259 */ 260 case 'required': 261 if (!array_key_exists($field, $this->input)) { 262 $this->addError($fatal, _s('Field "%1$s" is mandatory.', $field)); 263 return false; 264 } 265 break; 266 267 /* 268 * 'range_time' => true 269 */ 270 case 'range_time': 271 if (array_key_exists($field, $this->input) && !$this->isRangeTime($this->input[$field])) { 272 $this->addError($fatal, 273 _s('Incorrect value for field "%1$s": %2$s.', $field, _('a time is expected')) 274 ); 275 return false; 276 } 277 break; 278 279 /* 280 * 'time_periods' => true 281 */ 282 case 'time_periods': 283 if (array_key_exists($field, $this->input) && !$this->isTimePeriods($this->input[$field])) { 284 $this->addError($fatal, 285 _s('Incorrect value for field "%1$s": %2$s.', $field, _('a time period is expected')) 286 ); 287 return false; 288 } 289 break; 290 291 /* 292 * 'rgb' => true 293 */ 294 case 'rgb': 295 if (array_key_exists($field, $this->input) && !$this->isRgb($this->input[$field])) { 296 $this->addError($fatal, 297 _s('Incorrect value for field "%1$s": %2$s.', $field, 298 _('a hexadecimal colour code (6 symbols) is expected') 299 ) 300 ); 301 return false; 302 } 303 break; 304 305 /* 306 * 'string' => true 307 */ 308 case 'string': 309 if (array_key_exists($field, $this->input) && !is_string($this->input[$field])) { 310 $this->addError($fatal, 311 _s('Incorrect value for field "%1$s": %2$s.', $field, _('a character string is expected')) 312 ); 313 return false; 314 } 315 break; 316 317 /* 318 * 'flags' => <value1> | <value2> | ... | <valueN> 319 */ 320 case 'flags': 321 break; 322 323 default: 324 // the message can be not translated because it is an internal error 325 $this->addError($fatal, 'Invalid validation rule "'.$rule.'".'); 326 return false; 327 } 328 } 329 330 return true; 331 } 332 333 public static function is_id($value) { 334 if (!preg_match('/^'.ZBX_PREG_INT.'$/', $value)) { 335 return false; 336 } 337 338 return (bccomp($value, '0') >= 0 && bccomp($value, ZBX_DB_MAX_ID) <= 0); 339 } 340 341 public static function is_int32($value) { 342 if (!preg_match('/^'.ZBX_PREG_INT.'$/', $value)) { 343 return false; 344 } 345 346 return ($value >= ZBX_MIN_INT32 && $value <= ZBX_MAX_INT32); 347 } 348 349 public static function is_uint64($value) { 350 if (!preg_match('/^'.ZBX_PREG_INT.'$/', $value)) { 351 return false; 352 } 353 354 return ($value >= 0 && bccomp($value, ZBX_MAX_UINT64) <= 0); 355 } 356 357 /** 358 * Validate value against DB schema. 359 * 360 * @param array $field_schema Array of DB schema. 361 * @param string $field_schema['type'] Type of DB field. 362 * @param string $field_schema['length'] Length of DB field. 363 * @param string $value [IN/OUT] IN - input value, OUT - changed value according to flags. 364 * @param int $flags Validation flags. 365 * 366 * @return bool 367 */ 368 private function check_db_value($field_schema, &$value, $flags) { 369 switch ($field_schema['type']) { 370 case DB::FIELD_TYPE_ID: 371 return self::is_id($value); 372 373 case DB::FIELD_TYPE_INT: 374 return self::is_int32($value); 375 376 case DB::FIELD_TYPE_CHAR: 377 if ($flags & P_CRLF) { 378 $value = CRLFtoLF($value); 379 } 380 381 return (mb_strlen($value) <= $field_schema['length']); 382 383 case DB::FIELD_TYPE_NCLOB: 384 case DB::FIELD_TYPE_TEXT: 385 if ($flags & P_CRLF) { 386 $value = CRLFtoLF($value); 387 } 388 389 // TODO: check length 390 return true; 391 392 default: 393 return false; 394 } 395 } 396 397 private function is_array_id(array $values) { 398 foreach ($values as $value) { 399 if (!is_string($value) || !self::is_id($value)) { 400 return false; 401 } 402 } 403 404 return true; 405 } 406 407 /** 408 * Validate array of string values against DB schema. 409 * 410 * @param array $values [IN/OUT] IN - input values, OUT - changed values according to flags. 411 * @param string $table DB table name. 412 * @param string $field DB field name. 413 * @param int $flags Validation flags. 414 * 415 * @return bool 416 */ 417 private function is_array_db(array &$values, $table, $field, $flags) { 418 $table_schema = DB::getSchema($table); 419 420 foreach ($values as &$value) { 421 if (!is_string($value) || !$this->check_db_value($table_schema['fields'][$field], $value, $flags)) { 422 return false; 423 } 424 } 425 unset($value); 426 427 return true; 428 } 429 430 /** 431 * Validate a string value against DB schema. 432 * 433 * @param string $value [IN/OUT] IN - input value, OUT - changed value according to flags. 434 * @param string $table DB table name. 435 * @param string $field DB field name. 436 * @param int $flags Validation flags. 437 * 438 * @return bool 439 */ 440 private function is_db(&$value, $table, $field, $flags) { 441 $table_schema = DB::getSchema($table); 442 443 return (is_string($value) && $this->check_db_value($table_schema['fields'][$field], $value, $flags)); 444 } 445 446 private function isTimePeriods($value) { 447 if ($this->time_periods_parser === null) { 448 $this->time_periods_parser = new CTimePeriodsParser(['usermacros' => true]); 449 } 450 451 return is_string($value) && $this->time_periods_parser->parse($value) == CParser::PARSE_SUCCESS; 452 } 453 454 private function isRangeTime($value) { 455 if ($this->range_time_parser === null) { 456 $this->range_time_parser = new CRangeTimeParser(); 457 } 458 459 return is_string($value) && $this->range_time_parser->parse($value) == CParser::PARSE_SUCCESS; 460 } 461 462 private function isRgb($value) { 463 return is_string($value) && preg_match('/^[A-F0-9]{6}$/', $value); 464 } 465 466 /** 467 * Add validation error. 468 * 469 * @return string 470 */ 471 public function addError($fatal, $error) { 472 if ($fatal) { 473 $this->errorsFatal[] = $error; 474 } 475 else { 476 $this->errors[] = $error; 477 } 478 } 479 480 /** 481 * Get valid fields. 482 * 483 * @return array of fields passed validation 484 */ 485 public function getValidInput() { 486 return $this->output; 487 } 488 489 /** 490 * Returns array of error messages. 491 * 492 * @return array 493 */ 494 public function getAllErrors() { 495 return array_merge($this->errorsFatal, $this->errors); 496 } 497 498 /** 499 * Returns true if validation failed with errors. 500 * 501 * @return bool 502 */ 503 public function isError() { 504 return (bool) $this->errors; 505 } 506 507 /** 508 * Returns true if validation failed with fatal errors. 509 * 510 * @return bool 511 */ 512 public function isErrorFatal() { 513 return (bool) $this->errorsFatal; 514 } 515} 516