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