<?php
/*
** Zabbix
** Copyright (C) 2001-2022 Zabbix SIA
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
**/


/**
 * API validator
 */
class CApiInputValidator {

	/**
	 * Base validation function.
	 *
	 * @param array  $rule  validation rule
	 * @param mixed  $data  import data
	 * @param string $path  data path (for error reporting)
	 * @param string $error
	 *
	 * @return bool
	 */
	public static function validate(array $rule, &$data, $path, &$error) {
		$error = '';

		return self::validateData($rule, $data, $path, $error)
			&& self::validateDataUniqueness($rule, $data, $path, $error);
	}

	/**
	 * Base uniqueness validation function.
	 *
	 * @param array  $rule  validation rule
	 * @param mixed  $data  import data
	 * @param string $path  data path (for error reporting)
	 * @param string $error
	 *
	 * @return bool
	 */
	public static function validateUniqueness(array $rule, $data, $path, &$error) {
		$error = '';

		return self::validateDataUniqueness($rule, $data, $path, $error);
	}

	/**
	 * Base data validation function.
	 *
	 * @param array  $rule
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateData($rule, &$data, $path, &$error) {
		switch ($rule['type']) {
			case API_CALC_FORMULA:
				return self::validateCalcFormula($rule, $data, $path, $error);

			case API_COLOR:
				return self::validateColor($rule, $data, $path, $error);

			case API_COND_FORMULA:
				return self::validateCondFormula($rule, $data, $path, $error);

			case API_COND_FORMULAID:
				return self::validateCondFormulaId($rule, $data, $path, $error);

			case API_STRING_UTF8:
				return self::validateStringUtf8($rule, $data, $path, $error);

			case API_STRINGS_UTF8:
				return self::validateStringsUtf8($rule, $data, $path, $error);

			case API_INT32:
				return self::validateInt32($rule, $data, $path, $error);

			case API_INTS32:
				return self::validateInts32($rule, $data, $path, $error);

			case API_INT32_RANGES:
				return self::validateInt32Ranges($rule, $data, $path, $error);

			case API_UINT64:
				return self::validateUInt64($rule, $data, $path, $error);

			case API_UINTS64:
				return self::validateUInts64($rule, $data, $path, $error);

			case API_FLOAT:
				return self::validateFloat($rule, $data, $path, $error);

			case API_FLOATS:
				return self::validateFloats($rule, $data, $path, $error);

			case API_ID:
				return self::validateId($rule, $data, $path, $error);

			case API_BOOLEAN:
				return self::validateBoolean($rule, $data, $path, $error);

			case API_FLAG:
				return self::validateFlag($rule, $data, $path, $error);

			case API_OBJECT:
				return self::validateObject($rule, $data, $path, $error);

			case API_OUTPUT:
				return self::validateOutput($rule, $data, $path, $error);

			case API_PSK:
				return self::validatePSK($rule, $data, $path, $error);

			case API_SORTORDER:
				return self::validateSortOrder($rule, $data, $path, $error);

			case API_IDS:
				return self::validateIds($rule, $data, $path, $error);

			case API_OBJECTS:
				return self::validateObjects($rule, $data, $path, $error);

			case API_HG_NAME:
				return self::validateHostGroupName($rule, $data, $path, $error);

			case API_H_NAME:
				return self::validateHostName($rule, $data, $path, $error);

			case API_NUMERIC:
				return self::validateNumeric($rule, $data, $path, $error);

			case API_SCRIPT_MENU_PATH:
				return self::validateScriptMenuPath($rule, $data, $path, $error);

			case API_USER_MACRO:
				return self::validateUserMacro($rule, $data, $path, $error);

			case API_USER_MACROS:
				return self::validateUserMacros($rule, $data, $path, $error);

			case API_LLD_MACRO:
				return self::validateLLDMacro($rule, $data, $path, $error);

			case API_TIME_PERIOD:
				return self::validateTimePeriod($rule, $data, $path, $error);

			case API_REGEX:
				return self::validateRegex($rule, $data, $path, $error);

			case API_HTTP_POST:
				return self::validateHttpPosts($rule, $data, $path, $error);

			case API_VARIABLE_NAME:
				return self::validateVariableName($rule, $data, $path, $error);

			case API_TIME_UNIT:
				return self::validateTimeUnit($rule, $data, $path, $error);

			case API_URL:
				return self::validateUrl($rule, $data, $path, $error);

			case API_IP:
				return self::validateIp($rule, $data, $path, $error);

			case API_IP_RANGES:
				return self::validateIpRanges($rule, $data, $path, $error);

			case API_DNS:
				return self::validateDns($rule, $data, $path, $error);

			case API_PORT:
				return self::validatePort($rule, $data, $path, $error);

			case API_TRIGGER_EXPRESSION:
				return self::validateTriggerExpression($rule, $data, $path, $error);

			case API_EVENT_NAME:
				return self::validateEventName($rule, $data, $path, $error);

			case API_JSONRPC_PARAMS:
				return self::validateJsonRpcParams($rule, $data, $path, $error);

			case API_JSONRPC_ID:
				return self::validateJsonRpcId($rule, $data, $path, $error);

			case API_DATE:
				return self::validateDate($rule, $data, $path, $error);

			case API_NUMERIC_RANGES:
				return self::validateNumericRanges($rule, $data, $path, $error);

			case API_UUID:
				return self::validateUuid($rule, $data, $path, $error);

			case API_CUIDS:
				return self::validateCuids($rule, $data, $path, $error);

			case API_CUID:
				return self::validateCuid($rule, $data, $path, $error);

			case API_VAULT_SECRET:
				return self::validateVaultSecret($rule, $data, $path, $error);

			case API_IMAGE:
				return self::validateImage($rule, $data, $path, $error);

			case API_EXEC_PARAMS:
				return self::validateExecParams($rule, $data, $path, $error);

			case API_LAT_LNG_ZOOM:
				return self::validateLatLngZoom($rule, $data, $path, $error);

			case API_TIMESTAMP:
				return self::validateTimestamp($rule, $data, $path, $error);
		}

		// This message can be untranslated because warn about incorrect validation rules at a development stage.
		$error = 'Incorrect validation rules.';

		return false;
	}

	/**
	 * Base data uniqueness validation function.
	 *
	 * @param array  $rule
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateDataUniqueness($rule, &$data, $path, &$error) {
		switch ($rule['type']) {
			case API_CALC_FORMULA:
			case API_COLOR:
			case API_COND_FORMULA:
			case API_COND_FORMULAID:
			case API_STRING_UTF8:
			case API_INT32:
			case API_INT32_RANGES:
			case API_UINT64:
			case API_UINTS64:
			case API_FLOAT:
			case API_FLOATS:
			case API_ID:
			case API_BOOLEAN:
			case API_FLAG:
			case API_OUTPUT:
			case API_PSK:
			case API_SORTORDER:
			case API_HG_NAME:
			case API_H_NAME:
			case API_NUMERIC:
			case API_SCRIPT_MENU_PATH:
			case API_USER_MACRO:
			case API_LLD_MACRO:
			case API_TIME_PERIOD:
			case API_TIME_UNIT:
			case API_REGEX:
			case API_HTTP_POST:
			case API_VARIABLE_NAME:
			case API_URL:
			case API_IP:
			case API_IP_RANGES:
			case API_DNS:
			case API_PORT:
			case API_TRIGGER_EXPRESSION:
			case API_EVENT_NAME:
			case API_JSONRPC_PARAMS:
			case API_JSONRPC_ID:
			case API_DATE:
			case API_NUMERIC_RANGES:
			case API_UUID:
			case API_CUID:
			case API_VAULT_SECRET:
			case API_IMAGE:
			case API_EXEC_PARAMS:
			case API_UNEXPECTED:
			case API_LAT_LNG_ZOOM:
			case API_TIMESTAMP:
				return true;

			case API_OBJECT:
				foreach ($rule['fields'] as $field_name => $field_rule) {
					if ($data !== null && array_key_exists($field_name, $data)) {
						if ($field_rule['type'] === API_MULTIPLE) {
							foreach ($field_rule['rules'] as $multiple_rule) {
								if (array_key_exists('else', $multiple_rule)
										|| (is_array($multiple_rule['if'])
											&& self::isInRange($data[$multiple_rule['if']['field']], $multiple_rule['if']['in']))
										|| ($multiple_rule['if'] instanceof Closure
											&& call_user_func($multiple_rule['if'], $data))) {
									$field_rule = $multiple_rule;
									break;
								}
							}
						}

						$subpath = ($path === '/' ? $path : $path.'/').$field_name;
						if (!self::validateDataUniqueness($field_rule, $data[$field_name], $subpath, $error)) {
							return false;
						}
					}
				}
				return true;

			case API_IDS:
			case API_STRINGS_UTF8:
			case API_INTS32:
			case API_CUIDS:
			case API_USER_MACROS:
				return self::validateStringsUniqueness($rule, $data, $path, $error);

			case API_OBJECTS:
				return self::validateObjectsUniqueness($rule, $data, $path, $error);
		}

		// This message can be untranslated because warn about incorrect validation rules at a development stage.
		$error = 'Incorrect validation rules.';

		return false;
	}

	/**
	 * Generic string validator.
	 *
	 * @param int    $flags  API_NOT_EMPTY
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function checkStringUtf8($flags, &$data, $path, &$error) {
		if (!is_string($data)) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('a character string is expected'));
			return false;
		}

		if (mb_check_encoding($data, 'UTF-8') !== true) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('invalid byte sequence in UTF-8'));
			return false;
		}

		if (($flags & API_NOT_EMPTY) && $data === '') {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('cannot be empty'));
			return false;
		}

		return true;
	}

	/**
	 * Calculated item formula validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']   (optional) API_ALLOW_LLD_MACRO
	 * @param int    $rule['length']  (optional)
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateCalcFormula($rule, &$data, $path, &$error) {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (self::checkStringUtf8(API_NOT_EMPTY, $data, $path, $error) === false) {
			return false;
		}

		if (array_key_exists('length', $rule) && mb_strlen($data) > $rule['length']) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('value is too long'));
			return false;
		}

		$expression_parser = new CExpressionParser([
			'usermacros' => true,
			'lldmacros' => ($flags & API_ALLOW_LLD_MACRO),
			'calculated' => true,
			'host_macro' => true,
			'empty_host' => true
		]);

		if ($expression_parser->parse($data) != CParser::PARSE_SUCCESS) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, $expression_parser->getError());
			return false;
		}

		$expression_validator = new CExpressionValidator([
			'usermacros' => true,
			'lldmacros' => ($flags & API_ALLOW_LLD_MACRO),
			'calculated' => true
		]);

		if (!$expression_validator->validate($expression_parser->getResult()->getTokens())) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, $expression_validator->getError());
			return false;
		}

		return true;
	}

	/**
	 * Color validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']   (optional) API_NOT_EMPTY, API_ALLOW_NULL
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateColor($rule, &$data, $path, &$error) {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (($flags & API_ALLOW_NULL) && $data === null) {
			return true;
		}

		if (self::checkStringUtf8($flags & API_NOT_EMPTY, $data, $path, $error) === false) {
			return false;
		}

		if (($flags & API_NOT_EMPTY) == 0 && $data === '') {
			return true;
		}

		if (preg_match('/^[0-9a-f]{6}$/i', $data) !== 1) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path,
				_('a hexadecimal color code (6 symbols) is expected')
			);
			return false;
		}

		return true;
	}

	/**
	 * Calculated condition formula validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['length']  (optional)
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateCondFormula(array $rule, &$data, string $path, string &$error): bool {
		if (self::checkStringUtf8(API_NOT_EMPTY, $data, $path, $error) === false) {
			return false;
		}

		if (array_key_exists('length', $rule) && mb_strlen($data) > $rule['length']) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('value is too long'));
			return false;
		}

		$condition_formula_parser = new CConditionFormula();

		if (!$condition_formula_parser->parse($data)) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, $condition_formula_parser->error);
			return false;
		}

		return true;
	}

	/**
	 * Calculated condition formula ID validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['length']  (optional)
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateCondFormulaId(array $rule, &$data, string $path, string &$error): bool {
		if (self::checkStringUtf8(API_NOT_EMPTY, $data, $path, $error) === false) {
			return false;
		}

		if (array_key_exists('length', $rule) && mb_strlen($data) > $rule['length']) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('value is too long'));
			return false;
		}

		if (preg_match('/^[A-Z]+$/', $data) !== 1) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('uppercase identifier expected'));
			return false;
		}

		return true;
	}

	/**
	 * Returns unescaped array of "in" rules.
	 *
	 * @static
	 *
	 * @param string $in  A comma-delimited character string. For example, 'xml,json' or '\,,.,/'.
	 *
	 * @return array  An array of "in" rules. For example, ['xml', 'json'] or [',', '.', '/'].
	 */
	private static function unescapeInRule(string $in): array {
		$result = [];
		$pos = 0;

		do {
			preg_match('/^([^,\\\\]|\\\\[,\\\\])*/', substr($in, $pos), $matches);
			$result[] = strtr($matches[0], ['\\,' => ',', '\\\\' => '\\']);
			$pos += strlen($matches[0]);

			if (!isset($in[$pos])) {
				break;
			}

			$pos++;
		}
		while (true);

		return $result;
	}

	/**
	 * String validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']   (optional) API_NOT_EMPTY, API_ALLOW_NULL
	 * @param string $rule['in']      (optional) A comma-delimited character string, for example: 'xml,json'.
	 *                                           Comma and backslash char can be escaped.
	 * @param int    $rule['length']  (optional)
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateStringUtf8($rule, &$data, $path, &$error) {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (($flags & API_ALLOW_NULL) && $data === null) {
			return true;
		}

		if (self::checkStringUtf8($flags, $data, $path, $error) === false) {
			return false;
		}

		if (array_key_exists('in', $rule)) {
			$in = self::unescapeInRule($rule['in']);

			if (!in_array($data, $in, true)) {
				if (($i = array_search('', $in)) !== false) {
					unset($in[$i]);
				}

				if ($i === false) {
					$error = _n('value must be %1$s', 'value must be one of %1$s', '"'.implode('", "', $in).'"',
						count($in)
					);
				}
				elseif ($in) {
					$error = _n('value must be empty or %1$s', 'value must be empty or one of %1$s',
						'"'.implode('", "', $in).'"', count($in)
					);
				}
				else {
					$error = _s('value must be empty');
				}

				$error = _s('Invalid parameter "%1$s": %2$s.', $path, $error);
				return false;
			}
		}

		if (array_key_exists('length', $rule) && mb_strlen($data) > $rule['length']) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('value is too long'));
			return false;
		}

		return true;
	}

	/**
	 * Array of strings validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']   (optional) API_NOT_EMPTY, API_ALLOW_NULL, API_NORMALIZE
	 * @param string $rule['in']      (optional) a comma-delimited character string, for example: 'xml,json'
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateStringsUtf8($rule, &$data, $path, &$error) {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (($flags & API_ALLOW_NULL) && $data === null) {
			return true;
		}

		if (($flags & API_NORMALIZE) && self::validateStringUtf8([], $data, '', $e)) {
			$data = [$data];
		}
		unset($e);

		if (!is_array($data)) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('an array is expected'));
			return false;
		}

		if (($flags & API_NOT_EMPTY) && !$data) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('cannot be empty'));
			return false;
		}

		$data = array_values($data);
		$rules = ['type' => API_STRING_UTF8];

		if (array_key_exists('in', $rule)) {
			$rules['in'] = $rule['in'];
		}

		foreach ($data as $index => &$value) {
			$subpath = ($path === '/' ? $path : $path.'/').($index + 1);
			if (!self::validateData($rules, $value, $subpath, $error)) {
				return false;
			}
		}
		unset($value);

		return true;
	}

	/**
	 * Integers validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']   (optional) API_ALLOW_NULL
	 * @param string $rule['in']      (optional) a comma-delimited character string, for example: '0,60:900'
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateInt32($rule, &$data, $path, &$error) {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (($flags & API_ALLOW_NULL) && $data === null) {
			return true;
		}

		if ((!is_int($data) && !is_string($data)) || !preg_match('/^'.ZBX_PREG_INT.'$/', strval($data))) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('an integer is expected'));
			return false;
		}

		if (bccomp($data, ZBX_MIN_INT32) == -1 || bccomp($data, ZBX_MAX_INT32) == 1) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('a number is too large'));
			return false;
		}

		if (!self::checkInt32In($rule, $data, $path, $error)) {
			return false;
		}

		if (is_string($data)) {
			$data = (int) $data;
		}

		return true;
	}

	/**
	 * Unsigned integers validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']   (optional) API_ALLOW_NULL
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateUInt64($rule, &$data, $path, &$error) {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (($flags & API_ALLOW_NULL) && $data === null) {
			return true;
		}

		if (!is_scalar($data) || is_bool($data) || is_double($data) || !ctype_digit(strval($data))) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('an unsigned integer is expected'));
			return false;
		}

		if (bccomp($data, ZBX_MAX_UINT64) > 0) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('a number is too large'));
			return false;
		}

		$data = (string) $data;

		if ($data[0] === '0') {
			$data = ltrim($data, '0');

			if ($data === '') {
				$data = '0';
			}
		}

		return true;
	}

	/**
	 * Array of unsigned integers validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']   (optional) API_NOT_EMPTY, API_ALLOW_NULL, API_NORMALIZE
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateUInts64($rule, &$data, $path, &$error) {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (($flags & API_ALLOW_NULL) && $data === null) {
			return true;
		}

		if (($flags & API_NORMALIZE) && self::validateUInt64([], $data, '', $e)) {
			$data = [$data];
		}
		unset($e);

		if (!is_array($data)) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('an array is expected'));
			return false;
		}

		if (($flags & API_NOT_EMPTY) && !$data) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('cannot be empty'));
			return false;
		}

		$data = array_values($data);
		$rules = ['type' => API_UINT64];

		foreach ($data as $index => &$value) {
			$subpath = ($path === '/' ? $path : $path.'/').($index + 1);
			if (!self::validateData($rules, $value, $subpath, $error)) {
				return false;
			}
		}
		unset($value);

		return true;
	}

	/**
	 * Floating point number validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']   (optional) API_ALLOW_NULL
	 * @param string $rule['in']      (optional) a comma-delimited character string, for example: '0,60:900'
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateFloat($rule, &$data, $path, &$error) {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (($flags & API_ALLOW_NULL) && $data === null) {
			return true;
		}

		if (is_int($data) || is_float($data)) {
			$value = (float) $data;
		}
		elseif (is_string($data)) {
			$number_parser = new CNumberParser();

			if ($number_parser->parse($data) == CParser::PARSE_SUCCESS) {
				$value = (float) $number_parser->getMatch();
			}
			else {
				$value = NAN;
			}
		}
		else {
			$value = NAN;
		}

		if (is_nan($value)) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('a floating point value is expected'));

			return false;
		}

		if (!self::checkFloatIn($rule, $value, $path, $error)) {
			return false;
		}

		$data = $value;

		return true;
	}

	/**
	 * Array of floating point numbers validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']   (optional) API_NOT_EMPTY, API_ALLOW_NULL, API_NORMALIZE
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateFloats($rule, &$data, $path, &$error) {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (($flags & API_ALLOW_NULL) && $data === null) {
			return true;
		}

		if (($flags & API_NORMALIZE) && self::validateFloat([], $data, '', $e)) {
			$data = [$data];
		}
		unset($e);

		if (!is_array($data)) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('an array is expected'));
			return false;
		}

		if (($flags & API_NOT_EMPTY) && !$data) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('cannot be empty'));
			return false;
		}

		$data = array_values($data);
		$rules = ['type' => API_FLOAT];

		foreach ($data as $index => &$value) {
			$subpath = ($path === '/' ? $path : $path.'/').($index + 1);
			if (!self::validateData($rules, $value, $subpath, $error)) {
				return false;
			}
		}
		unset($value);

		return true;
	}

	private static function isInRange($data, $in) {
		$valid = false;

		foreach (explode(',', $in) as $in) {
			if (strpos($in, ':') !== false) {
				list($from, $to) = explode(':', $in);
			}
			else {
				$from = $in;
				$to = $in;
			}

			if ($from <= $data && $data <= $to) {
				$valid = true;
				break;
			}
		}

		return $valid;
	}

	/**
	 * @param array  $rule
	 * @param int    $rule['in']  (optional)
	 * @param int    $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function checkInt32In($rule, $data, $path, &$error) {
		if (!array_key_exists('in', $rule)) {
			return true;
		}

		$valid = self::isInRange($data, $rule['in']);

		if (!$valid) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _n('value must be %1$s', 'value must be one of %1$s',
				strtr($rule['in'], [',' => ', ', ':' => '-']), (strpbrk($rule['in'], ',:') === false) ? 1 : 2
			));
		}

		return $valid;
	}

	/**
	 * @param array  $rule
	 * @param int    $rule['in']  (optional)
	 * @param int    $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function checkFloatIn($rule, $data, $path, &$error) {
		if (!array_key_exists('in', $rule)) {
			return true;
		}

		$valid = self::isInRange($data, $rule['in']);

		if (!$valid) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path,
				_s('value must be within the range of %1$s', str_replace([',', ':'], [', ', '-'], $rule['in']))
			);
		}

		return $valid;
	}

	/**
	 * Array of integers validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']   (optional) API_NOT_EMPTY, API_ALLOW_NULL, API_NORMALIZE
	 * @param string $rule['in']      (optional) a comma-delimited character string, for example: '0,60:900'
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateInts32($rule, &$data, $path, &$error) {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (($flags & API_ALLOW_NULL) && $data === null) {
			return true;
		}

		if (($flags & API_NORMALIZE) && self::validateInt32([], $data, '', $e)) {
			$data = [$data];
		}
		unset($e);

		if (!is_array($data)) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('an array is expected'));
			return false;
		}

		if (($flags & API_NOT_EMPTY) && !$data) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('cannot be empty'));
			return false;
		}

		$data = array_values($data);
		$rules = ['type' => API_INT32];

		if (array_key_exists('in', $rule)) {
			$rules['in'] = $rule['in'];
		}

		foreach ($data as $index => &$value) {
			$subpath = ($path === '/' ? $path : $path.'/').($index + 1);
			if (!self::validateData($rules, $value, $subpath, $error)) {
				return false;
			}
		}
		unset($value);

		return true;
	}

	/**
	 * Validate integer ranges.
	 * Example:
	 *   0-100,200,300-400
	 *
	 * @static
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']   (optional) API_NOT_EMPTY
	 * @param int    $rule['length']  (optional)
	 * @param string $rule['in']      (optional) A comma-delimited character string, for example: '0,60:900'
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateInt32Ranges(array $rule, &$data, string $path, string &$error): bool {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (self::checkStringUtf8($flags, $data, $path, $error) === false) {
			return false;
		}

		if ($data === '') {
			return true;
		}

		if (array_key_exists('length', $rule) && mb_strlen($data) > $rule['length']) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('value is too long'));
			return false;
		}

		$parser = new CRangesParser(['with_minus' => true]);

		if ($parser->parse($data) != CParser::PARSE_SUCCESS) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('invalid range expression'));
			return false;
		}

		foreach ($parser->getRanges() as $ranges) {
			foreach ($ranges as $range) {
				if (!self::checkInt32In($rule, $range, $path, $error)) {
					return false;
				}
			}
		}

		return true;
	}

	/**
	 * Identifier validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']   (optional) API_ALLOW_NULL, API_NOT_EMPTY
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateId($rule, &$data, $path, &$error) {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (($flags & API_ALLOW_NULL) && $data === null) {
			return true;
		}

		if (!is_scalar($data) || is_bool($data) || is_double($data) || !ctype_digit(strval($data))) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('a number is expected'));
			return false;
		}

		if (($flags & API_NOT_EMPTY) && $data == 0) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('cannot be empty'));
			return false;
		}

		if (bccomp($data, ZBX_DB_MAX_ID) > 0) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('a number is too large'));
			return false;
		}

		$data = (string) $data;

		if ($data[0] === '0') {
			$data = ltrim($data, '0');

			if ($data === '') {
				$data = '0';
			}
		}

		return true;
	}

	/**
	 * Boolean validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']  (optional) API_ALLOW_NULL
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateBoolean($rule, &$data, $path, &$error) {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (($flags & API_ALLOW_NULL) && $data === null) {
			return true;
		}

		if (!is_bool($data)) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('a boolean is expected'));
			return false;
		}

		return true;
	}

	/**
	 * Flag validator.
	 *
	 * @param array  $rule
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateFlag($rule, &$data, $path, &$error) {
		if (is_bool($data)) {
			return true;
		}

		/**
		 * @deprecated  As of version 3.4, use boolean flags only.
		 */
		trigger_error(_('Non-boolean flags are deprecated.'), E_USER_NOTICE);

		$data = !is_null($data);

		return true;
	}

	/**
	 * Object validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']                                   (optional) API_ALLOW_NULL
	 * @param array  $rule['fields']
	 * @param int    $rule['fields'][<field_name>]['flags']           (optional) API_REQUIRED, API_DEPRECATED,
	 *                                                                           API_ALLOW_UNEXPECTED
	 * @param mixed  $rule['fields'][<field_name>]['default']         (optional)
	 * @param string $rule['fields'][<field_name>]['default_source']  (optional)
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateObject($rule, &$data, $path, &$error) {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (($flags & API_ALLOW_NULL) && $data === null) {
			return true;
		}

		if (!is_array($data)) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('an array is expected'));
			return false;
		}

		// unexpected parameter validation
		if (!($flags & API_ALLOW_UNEXPECTED)) {
			foreach ($data as $field_name => $value) {
				if (!$rule['fields']) {
					$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('should be empty'));
					return false;
				}

				if (!array_key_exists($field_name, $rule['fields'])) {
					$error = _s('Invalid parameter "%1$s": %2$s.', $path,
						_s('unexpected parameter "%1$s"', $field_name)
					);
					return false;
				}
			}
		}

		// validation of the values type
		foreach ($rule['fields'] as $field_name => $field_rule) {
			if ($field_rule['type'] === API_MULTIPLE) {
				foreach ($field_rule['rules'] as $multiple_rule) {
					if (array_key_exists('else', $multiple_rule)
							|| (is_array($multiple_rule['if'])
								&& self::isInRange($data[$multiple_rule['if']['field']], $multiple_rule['if']['in']))
							|| ($multiple_rule['if'] instanceof Closure
								&& call_user_func($multiple_rule['if'], $data))) {
						if ($multiple_rule['type'] == API_UNEXPECTED
								&& !self::validateUnexpected($field_name, $multiple_rule, $data, $path, $error)) {
							return false;
						}

						$field_rule += ['flags' => 0x00];
						$multiple_rule += ['flags' => 0x00];
						$multiple_rule['flags'] = ($field_rule['flags'] & API_REQUIRED) | $multiple_rule['flags'];
						$field_rule = $multiple_rule +
							array_intersect_key($field_rule, array_flip(['default', 'default_source']));
						break;
					}
				}

				if ($field_rule['type'] === API_MULTIPLE) {
					$error = 'Incorrect validation rules.';
					return false;
				}
			}
			elseif ($field_rule['type'] === API_UNEXPECTED
					&& !self::validateUnexpected($field_name, $field_rule, $data, $path, $error)) {
				return false;
			}

			$flags = array_key_exists('flags', $field_rule) ? $field_rule['flags'] : 0x00;

			if (array_key_exists('default', $field_rule) && !array_key_exists($field_name, $data)) {
				$data[$field_name] = $field_rule['default'];
			}

			if (array_key_exists('default_source', $field_rule) && !array_key_exists($field_name, $data)) {
				$data[$field_name] = $data[$field_rule['default_source']];
			}

			if (array_key_exists('compare', $field_rule)) {
				$field_rule['compare']['path'] = ($path === '/' ? $path : $path.'/').$field_rule['compare']['field'];
				$field_rule['compare']['value'] = $data[$field_rule['compare']['field']];
			}

			if (array_key_exists($field_name, $data)) {
				$subpath = ($path === '/' ? $path : $path.'/').$field_name;
				if (!self::validateData($field_rule, $data[$field_name], $subpath, $error)) {
					return false;
				}
				if ($flags & API_DEPRECATED) {
					trigger_error(_s('Parameter "%1$s" is deprecated.', $subpath), E_USER_NOTICE);
				}
			}
			elseif ($flags & API_REQUIRED) {
				$error = _s('Invalid parameter "%1$s": %2$s.', $path,
					_s('the parameter "%1$s" is missing', $field_name)
				);
				return false;
			}
		}

		return true;
	}

	/**
	 * API output validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']   (optional) API_ALLOW_COUNT, API_ALLOW_NULL
	 * @param string $rule['in']      (optional) comma-delimited field names, for example: 'hostid,name'
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateOutput($rule, &$data, $path, &$error) {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (($flags & API_ALLOW_NULL) && $data === null) {
			return true;
		}

		if (is_array($data)) {
			$rules = ['type' => API_STRINGS_UTF8, 'uniq' => true];

			if (array_key_exists('in', $rule)) {
				$rules['in'] = $rule['in'];
			}

			return self::validateData($rules, $data, $path, $error)
				&& self::validateDataUniqueness($rules, $data, $path, $error);
		}

		if (is_string($data)) {
			$in = ($flags & API_ALLOW_COUNT) ? implode(',', [API_OUTPUT_EXTEND, API_OUTPUT_COUNT]) : API_OUTPUT_EXTEND;

			return self::validateData(['type' => API_STRING_UTF8, 'in' => $in], $data, $path, $error);
		}

		$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('an array or a character string is expected'));

		return false;
	}

	/**
	 * PSK key validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']   (optional) API_NOT_EMPTY
	 * @param int    $rule['length']  (optional)
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validatePSK($rule, &$data, $path, &$error) {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (self::checkStringUtf8($flags & API_NOT_EMPTY, $data, $path, $error) === false) {
			return false;
		}

		$mb_len = mb_strlen($data);

		if ($mb_len != 0 && $mb_len < PSK_MIN_LEN) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _s('minimum length is %1$s characters', PSK_MIN_LEN));
			return false;
		}

		if (preg_match('/^([0-9a-f]{2})*$/i', $data) !== 1) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path,
				_('an even number of hexadecimal characters is expected')
			);
			return false;
		}

		if (array_key_exists('length', $rule) && $mb_len > $rule['length']) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('value is too long'));
			return false;
		}

		return true;
	}

	/**
	 * API sort order validator.
	 *
	 * @param array  $rule
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateSortOrder($rule, &$data, $path, &$error) {
		$in = ZBX_SORT_UP.','.ZBX_SORT_DOWN;

		if (self::validateStringUtf8(['in' => $in], $data, $path, $e)) {
			return true;
		}

		if (is_string($data)) {
			$error = $e;
			return false;
		}
		unset($e);

		if (!is_array($data)) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('an array or a character string is expected'));
			return false;
		}

		$data = array_values($data);
		$rules = [
			'type' => API_STRING_UTF8,
			'in' => $in
		];

		foreach ($data as $index => &$value) {
			$subpath = ($path === '/' ? $path : $path.'/').($index + 1);
			if (!self::validateData($rules, $value, $subpath, $error)) {
				return false;
			}
		}
		unset($value);

		return true;
	}

	/**
	 * Array of ids validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']   (optional) API_NOT_EMPTY, API_ALLOW_NULL, API_NORMALIZE
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateIds($rule, &$data, $path, &$error) {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (($flags & API_ALLOW_NULL) && $data === null) {
			return true;
		}

		if (($flags & API_NORMALIZE) && self::validateId([], $data, '', $e)) {
			$data = [$data];
		}
		unset($e);

		if (!is_array($data)) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('an array is expected'));
			return false;
		}

		if (($flags & API_NOT_EMPTY) && !$data) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('cannot be empty'));
			return false;
		}

		$data = array_values($data);

		foreach ($data as $index => &$value) {
			$subpath = ($path === '/' ? $path : $path.'/').($index + 1);
			if (!self::validateId([], $value, $subpath, $error)) {
				return false;
			}
		}
		unset($value);

		return true;
	}

	/**
	 * Array of objects validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']   (optional) API_NOT_EMPTY, API_ALLOW_NULL, API_NORMALIZE, API_PRESERVE_KEYS,
	 *                                           API_ALLOW_UNEXPECTED
	 * @param array  $rule['fields']
	 * @param int    $rule['length']  (optional)
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateObjects($rule, &$data, $path, &$error) {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (($flags & API_ALLOW_NULL) && $data === null) {
			return true;
		}

		if (!is_array($data)) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('an array is expected'));
			return false;
		}

		if (($flags & API_NOT_EMPTY) && !$data) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('cannot be empty'));
			return false;
		}

		if (array_key_exists('length', $rule) && count($data) > $rule['length']) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('value is too long'));
			return false;
		}

		if (($flags & API_NORMALIZE) && $data) {
			reset($data);

			if (!is_int(key($data))) {
				$data = [$data];
			}
		}

		if (!($flags & API_PRESERVE_KEYS)) {
			$data = array_values($data);
		}

		foreach ($data as $index => &$value) {
			$subpath = ($path === '/' ? $path : $path.'/').($index + 1);
			if (!self::validateObject(['flags' => ($flags & API_ALLOW_UNEXPECTED), 'fields' => $rule['fields']], $value,
					$subpath, $error)) {
				return false;
			}
		}
		unset($value);

		return true;
	}

	/**
	 * Host group name validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']   (optional) API_REQUIRED_LLD_MACRO
	 * @param int    $rule['length']  (optional)
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateHostGroupName($rule, &$data, $path, &$error) {
		if (self::checkStringUtf8(API_NOT_EMPTY, $data, $path, $error) === false) {
			return false;
		}

		if (array_key_exists('length', $rule) && mb_strlen($data) > $rule['length']) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('value is too long'));

			return false;
		}

		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;
		$host_group_name_parser = new CHostGroupNameParser(['lldmacros' => ($flags & API_REQUIRED_LLD_MACRO)]);

		if ($host_group_name_parser->parse($data) != CParser::PARSE_SUCCESS) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('invalid host group name'));

			return false;
		}

		if (($flags & API_REQUIRED_LLD_MACRO) && !$host_group_name_parser->getMacros()) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path,
				_('must contain at least one low-level discovery macro')
			);

			return false;
		}

		return true;
	}

	/**
	 * Host name validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']   (optional) API_REQUIRED_LLD_MACRO
	 * @param int    $rule['length']  (optional)
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateHostName($rule, &$data, $path, &$error) {
		if (self::checkStringUtf8(API_NOT_EMPTY, $data, $path, $error) === false) {
			return false;
		}

		if (array_key_exists('length', $rule) && mb_strlen($data) > $rule['length']) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('value is too long'));

			return false;
		}

		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;
		$host_name_parser = new CHostNameParser(['lldmacros' => ($flags & API_REQUIRED_LLD_MACRO)]);

		// For example, host prototype name MUST contain macros.
		if ($host_name_parser->parse($data) != CParser::PARSE_SUCCESS) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('invalid host name'));

			return false;
		}

		if (($flags & API_REQUIRED_LLD_MACRO) && !$host_name_parser->getMacros()) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path,
				_('must contain at least one low-level discovery macro')
			);

			return false;
		}

		return true;
	}

	/**
	 * Validator for numeric data with optional suffix.
	 * Supported time suffixes: s, m, h, d, w
	 * Supported metric suffixes: K, M, G, T
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']   (optional) API_NOT_EMPTY
	 * @param int    $rule['length']  (optional)
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateNumeric($rule, &$data, $path, &$error) {
		global $DB;

		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (is_int($data)) {
			$data = (string) $data;
		}

		if (self::checkStringUtf8($flags & API_NOT_EMPTY, $data, $path, $error) === false) {
			return false;
		}

		if (array_key_exists('length', $rule) && mb_strlen($data) > $rule['length']) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('value is too long'));

			return false;
		}

		if (($flags & API_NOT_EMPTY) == 0 && $data === '') {
			return true;
		}

		$number_parser = new CNumberParser(['with_size_suffix' => true, 'with_time_suffix' => true]);

		if ($number_parser->parse($data) != CParser::PARSE_SUCCESS) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('a number is expected'));

			return false;
		}

		$value = $number_parser->calcValue();

		if ($DB['DOUBLE_IEEE754']) {
			if (abs($value) > ZBX_FLOAT_MAX) {
				$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('a number is too large'));

				return false;
			}
		}
		else {
			if (abs($value) >= 1E+16) {
				$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('a number is too large'));

				return false;
			}
			elseif ($value != round($value, 4)) {
				$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('a number has too many fractional digits'));

				return false;
			}
		}

		// Remove leading zeros.
		$data = preg_replace('/^(-)?(0+)?(\d.*)$/', '${1}${3}', $data);

		// Add leading zero.
		$data = preg_replace('/^(-)?(\..*)$/', '${1}0${2}', $data);

		return true;
	}

	/**
	 * Global script menu_path validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']   (optional)
	 * @param int    $rule['length']  (optional)
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateScriptMenuPath($rule, &$data, $path, &$error) {
		// Having only a root folder is the same as being empty. Temporary modify data to check if it is actually empty.
		$tmp_data = $data;

		if ($tmp_data === '/') {
			$tmp_data = '';
		}

		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (self::checkStringUtf8($flags, $tmp_data, $path, $error) === false) {
			return false;
		}

		if (array_key_exists('length', $rule) && mb_strlen($data) > $rule['length']) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('value is too long'));
			return false;
		}

		// If empty is allowed there is only root folder, return early.
		if ($data === '/') {
			return true;
		}

		$folders = splitPath($data);
		$folders = array_map('trim', $folders);
		$count = count($folders);

		// folder1/{empty}/name or folder1/folder2/{empty}
		foreach ($folders as $num => $folder) {
			// Allow the trailing slash.
			if ($folder === '' && $num != ($count - 1) && $num != 0) {
				$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('directory cannot be empty'));

				return false;
			}
		}

		return true;
	}

	/**
	 * User macro validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['length']  (optional)
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateUserMacro($rule, &$data, $path, &$error) {
		if (self::checkStringUtf8(API_NOT_EMPTY, $data, $path, $error) === false) {
			return false;
		}

		if (array_key_exists('length', $rule) && mb_strlen($data) > $rule['length']) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('value is too long'));
			return false;
		}

		$user_macro_parser = new CUserMacroParser();

		if ($user_macro_parser->parse($data) != CParser::PARSE_SUCCESS) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, $user_macro_parser->getError());
			return false;
		}

		return true;
	}

	/**
	 * Array of strings validator.
	 *
	 * @param array   $rule
	 * @param int     $rule['flags']   (optional) API_NORMALIZE
	 * @param integer $rule['length']  (optional)
	 * @param mixed   $data
	 * @param string  $path
	 * @param string  $error
	 *
	 * @return bool
	 */
	private static function validateUserMacros(array $rule, &$data, string $path, ?string &$error): bool {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (($flags & API_NORMALIZE) && self::validateUserMacro([], $data, '', $e)) {
			$data = [$data];
		}
		unset($e);

		if (!is_array($data)) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('an array is expected'));
			return false;
		}

		$data = array_values($data);
		$rules = ['type' => API_USER_MACRO];

		if (array_key_exists('length', $rule)) {
			$rules['length'] = $rule['length'];
		}

		foreach ($data as $index => &$value) {
			$subpath = ($path === '/' ? $path : $path.'/').($index + 1);
			if (!self::validateData($rules, $value, $subpath, $error)) {
				return false;
			}
		}
		unset($value);

		return true;
	}

	/**
	 * LLD macro validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['length']  (optional)
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateLLDMacro($rule, &$data, $path, &$error) {
		if (self::checkStringUtf8(API_NOT_EMPTY, $data, $path, $error) === false) {
			return false;
		}

		if (array_key_exists('length', $rule) && mb_strlen($data) > $rule['length']) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('value is too long'));
			return false;
		}

		if ((new CLLDMacroParser())->parse($data) != CParser::PARSE_SUCCESS) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('a low-level discovery macro is expected'));
			return false;
		}

		return true;
	}

	/**
	 * Time period validator like "1-7,00:00-24:00".
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']   (optional) API_ALLOW_USER_MACRO
	 * @param int    $rule['length']  (optional)
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateTimePeriod($rule, &$data, $path, &$error) {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (self::checkStringUtf8(API_NOT_EMPTY, $data, $path, $error) === false) {
			return false;
		}

		if (array_key_exists('length', $rule) && mb_strlen($data) > $rule['length']) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('value is too long'));
			return false;
		}

		$time_period_parser = new CTimePeriodsParser(['usermacros' => ($flags & API_ALLOW_USER_MACRO)]);

		if ($time_period_parser->parse($data) != CParser::PARSE_SUCCESS) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('a time period is expected'));
			return false;
		}

		return true;
	}

	/**
	 * Regular expression validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']   (optional) API_NOT_EMPTY, API_ALLOW_GLOBAL_REGEX
	 * @param int    $rule['length']  (optional)
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateRegex($rule, &$data, $path, &$error) {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (self::checkStringUtf8($flags, $data, $path, $error) === false) {
			return false;
		}

		if (array_key_exists('length', $rule) && mb_strlen($data) > $rule['length']) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('value is too long'));
			return false;
		}

		if (($flags & API_ALLOW_GLOBAL_REGEX) && $data !== '' && $data[0] === '@') {
			return true;
		}

		if (@preg_match('/'.str_replace('/', '\/', $data).'/', '') === false) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('invalid regular expression'));
			return false;
		}

		return true;
	}

	/**
	 * Time unit validator like "10", "20s", "30m", "4h", "{$TIME}" etc.
	 *
	 * @param array  $rule
	 * @param int    $rule['length'] (optional)
	 * @param int    $rule['flags']  (optional) API_NOT_EMPTY, API_ALLOW_USER_MACRO, API_ALLOW_LLD_MACRO,
	 *                                          API_TIME_UNIT_WITH_YEAR
	 * @param int    $rule['in']     (optional)
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateTimeUnit($rule, &$data, $path, &$error) {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		/*
		 * It's possible to enter seconds as integers, but by default now we look for strings. For example: "30m".
		 * Other rules like emptiness and invalid characters are validated by parsers.
		 */
		if (is_int($data)) {
			$data = (string) $data;
		}

		if (self::checkStringUtf8($flags & API_NOT_EMPTY, $data, $path, $error) === false) {
			return false;
		}

		if (array_key_exists('length', $rule) && mb_strlen($data) > $rule['length']) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('value is too long'));
			return false;
		}

		if (($flags & API_NOT_EMPTY) == 0 && $data === '') {
			return true;
		}

		$simple_interval_parser = new CSimpleIntervalParser([
			'usermacros' => ($flags & API_ALLOW_USER_MACRO),
			'lldmacros' => ($flags & API_ALLOW_LLD_MACRO),
			'negative' => true,
			'with_year' => ($flags & API_TIME_UNIT_WITH_YEAR)
		]);

		if ($simple_interval_parser->parse($data) != CParser::PARSE_SUCCESS) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('a time unit is expected'));
			return false;
		}

		if (($flags & (API_ALLOW_USER_MACRO | API_ALLOW_LLD_MACRO)) && $data[0] === '{') {
			return true;
		}

		$seconds = timeUnitToSeconds($data, ($flags & API_TIME_UNIT_WITH_YEAR));

		if ($seconds < ZBX_MIN_INT32 || $seconds > ZBX_MAX_INT32) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('a number is too large'));
			return false;
		}

		return self::checkInt32In($rule, $seconds, $path, $error);
	}

	/**
	 * Array of ids, int32 or strings uniqueness validator.
	 *
	 * @param bool       $rule
	 * @param integer    $rule['type']
	 * @param bool       $rule['uniq']    (optional)
	 * @param array|null $data
	 * @param string     $path
	 * @param string     $error
	 *
	 * @return bool
	 */
	private static function validateStringsUniqueness($rule, ?array $data, $path, &$error) {
		// $data can be NULL when API_ALLOW_NULL is set
		if ($data === null) {
			return true;
		}

		if (!array_key_exists('uniq', $rule) || $rule['uniq'] === false) {
			return true;
		}

		$uniq = [];

		foreach ($data as $index => $value) {
			$uniq_value = ($rule['type'] == API_USER_MACROS)
				? self::trimMacro($value)
				: $value;

			if (array_key_exists($uniq_value, $uniq)) {
				$subpath = ($path === '/' ? $path : $path.'/').($index + 1);
				$error = _s('Invalid parameter "%1$s": %2$s.', $subpath,
					_s('value %1$s already exists', '('.$value.')')
				);
				return false;
			}
			$uniq[$uniq_value] = true;
		}

		return true;
	}

	/**
	 * Returns macro without spaces and curly braces.
	 *
	 * "{$MACRO}" => "MACRO"
	 * "{$MACRO:}" => "MACRO:context:"
	 * "{$MACRO: /var}" => "MACRO:context:/var"
	 * "{$MACRO: /"var"}" => "MACRO:context:/var"
	 * "{$MACRO:regex: ^[a-z]+}" => "MACRO:regex:^[a-z]+"
	 *
	 * @param string $macro
	 *
	 * @return string
	 */
	public static function trimMacro(string $macro): string {
		$user_macro_parser = new CUserMacroParser();

		$user_macro_parser->parse($macro);

		$macro = $user_macro_parser->getMacro();
		$context = $user_macro_parser->getContext();
		$regex = $user_macro_parser->getRegex();

		if ($context !== null) {
			$macro .= ':context:'.$context;
		}
		elseif ($regex !== null) {
			$macro .= ':regex:'.$regex;
		}

		return $macro;
	}

	/**
	 * Array of objects uniqueness validator.
	 *
	 * @param array      $rule
	 * @param array      $rule['uniq']    (optional) subsets of unique fields ([['hostid', 'name'], [...]])
	 * @param array      $rule['fields']
	 * @param array|null $data
	 * @param string     $path
	 * @param string     $error
	 *
	 * @return bool
	 */
	private static function validateObjectsUniqueness($rule, ?array $data, $path, &$error) {
		// $data can be NULL when API_ALLOW_NULL is set
		if ($data === null) {
			return true;
		}

		if (array_key_exists('uniq', $rule)) {
			foreach ($rule['uniq'] as $field_names) {
				$uniq = [];

				foreach ($data as $index => $object) {
					$_uniq = &$uniq;
					$values = [];
					$level = 1;

					foreach ($field_names as $field_name) {
						if (!array_key_exists($field_name, $object)) {
							break;
						}

						$values[] = $object[$field_name];

						$value = ($rule['fields'][$field_name]['type'] == API_USER_MACRO)
							? self::trimMacro($object[$field_name])
							: $object[$field_name];

						if ($level < count($field_names)) {
							if (!array_key_exists($value, $_uniq)) {
								$_uniq[$value] = [];
							}

							$_uniq = &$_uniq[$value];
						}
						else {
							if (array_key_exists($value, $_uniq)) {
								$subpath = ($path === '/' ? $path : $path.'/').($index + 1);
								$error = _s('Invalid parameter "%1$s": %2$s.', $subpath, _s('value %1$s already exists',
									'('.implode(', ', $field_names).')=('.implode(', ', $values).')'
								));
								return false;
							}

							$_uniq[$value] = true;
						}

						$level++;
					}
				}
			}
		}

		foreach ($data as $index => $object) {
			foreach ($rule['fields'] as $field_name => $field_rule) {
				if (array_key_exists($field_name, $object)) {
					if ($field_rule['type'] === API_MULTIPLE) {
						foreach ($field_rule['rules'] as $multiple_rule) {
							if (array_key_exists('else', $multiple_rule)
									|| (is_array($multiple_rule['if'])
										&& self::isInRange($object[$multiple_rule['if']['field']], $multiple_rule['if']['in']))
									|| ($multiple_rule['if'] instanceof Closure
										&& call_user_func($multiple_rule['if'], $object))) {
								$field_rule = $multiple_rule;
								break;
							}
						}
					}

					$subpath = ($path === '/' ? $path : $path.'/').($index + 1).'/'.$field_name;
					if (!self::validateDataUniqueness($field_rule, $object[$field_name], $subpath, $error)) {
						return false;
					}
				}
			}
		}

		return true;
	}

	/**
	 * HTTP POST validator. Posts can be set to string (raw post) or to http pairs (form fields)
	 *
	 * @param array  $rule
	 * @param int    $rule['length']        (optional)
	 * @param int    $rule['name-length']   (optional)
	 * @param int    $rule['value-length']  (optional)
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateHttpPosts($rule, &$data, $path, &$error) {
		if (is_array($data)) {
			$rules = ['type' => API_OBJECTS, 'fields' => [
				'name' =>	['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY],
				'value' =>	['type' => API_STRING_UTF8, 'flags' => API_REQUIRED]
			]];

			if (array_key_exists('name-length', $rule)) {
				$rules['fields']['name']['length'] = $rule['name-length'];
			}

			if (array_key_exists('value-length', $rule)) {
				$rules['fields']['value']['length'] = $rule['value-length'];
			}
		}
		else {
			$rules = ['type' => API_STRING_UTF8];

			if (array_key_exists('length', $rule)) {
				$rules['length'] = $rule['length'];
			}
		}

		return self::validateData($rules, $data, $path, $error);
	}

	/**
	 * HTTP variable validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['length']  (optional)
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateVariableName($rule, &$data, $path, &$error) {
		if (self::checkStringUtf8(API_NOT_EMPTY, $data, $path, $error) === false) {
			return false;
		}

		if (array_key_exists('length', $rule) && mb_strlen($data) > $rule['length']) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('value is too long'));
			return false;
		}

		if (preg_match('/^{[^{}]+}$/', $data) !== 1) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('is not enclosed in {} or is malformed'));
			return false;
		}

		return true;
	}

	/**
	 * URL validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['length']  (optional)
	 * @param int    $rule['flags']   (optional) API_ALLOW_USER_MACRO, API_ALLOW_EVENT_TAGS_MACRO, API_NOT_EMPTY
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateUrl($rule, &$data, $path, &$error) {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (self::checkStringUtf8($flags & API_NOT_EMPTY, $data, $path, $error) === false) {
			return false;
		}

		if (array_key_exists('length', $rule) && mb_strlen($data) > $rule['length']) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('value is too long'));
			return false;
		}

		$options = [
			'allow_user_macro' => (bool) ($flags & API_ALLOW_USER_MACRO),
			'allow_event_tags_macro' => (bool) ($flags & API_ALLOW_EVENT_TAGS_MACRO)
		];

		if ($data !== '' && CHtmlUrlValidator::validate($data, $options) === false) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('unacceptable URL'));
			return false;
		}

		return true;
	}

	/**
	 * IP address validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']   (optional) API_NOT_EMPTY, API_ALLOW_USER_MACRO, API_ALLOW_LLD_MACRO,
	 *                                API_ALLOW_MACRO
	 * @param int    $rule['length']  (optional)
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateIp($rule, &$data, $path, &$error) {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (self::checkStringUtf8($flags & API_NOT_EMPTY, $data, $path, $error) === false) {
			return false;
		}

		if (($flags & API_NOT_EMPTY) == 0 && $data === '') {
			return true;
		}

		if (array_key_exists('length', $rule) && mb_strlen($data) > $rule['length']) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('value is too long'));
			return false;
		}

		$ip_parser = new CIPParser([
			'v6' => ZBX_HAVE_IPV6,
			'usermacros' => ($flags & API_ALLOW_USER_MACRO),
			'lldmacros' => ($flags & API_ALLOW_LLD_MACRO),
			'macros' => ($flags & API_ALLOW_MACRO)
		]);

		if ($ip_parser->parse($data) != CParser::PARSE_SUCCESS) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('an IP address is expected'));
			return false;
		}

		return true;
	}

	/**
	 * Validate IP ranges. Multiple IPs separated by comma character.
	 * Example:
	 *   127.0.0.1,192.168.1.1-254,192.168.2.1-100,192.168.3.0/24
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']   (optional) API_NOT_EMPTY, API_ALLOW_DNS, API_ALLOW_RANGE
	 * @param int    $rule['length']  (optional)
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateIpRanges($rule, &$data, $path, &$error) {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (self::checkStringUtf8($flags, $data, $path, $error) === false) {
			return false;
		}

		if ($data === '') {
			return true;
		}

		if (array_key_exists('length', $rule) && mb_strlen($data) > $rule['length']) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('value is too long'));
			return false;
		}

		$ip_range_parser = new CIPRangeParser([
			'v6' => ZBX_HAVE_IPV6,
			'dns' => ($flags & API_ALLOW_DNS),
			'ranges' => ($flags & API_ALLOW_RANGE)
		]);

		if (!$ip_range_parser->parse($data)) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, $ip_range_parser->getError());
			return false;
		}

		return true;
	}

	/**
	 * DNS name validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']   (optional) API_NOT_EMPTY, API_ALLOW_USER_MACRO, API_ALLOW_LLD_MACRO,
	 *                                API_ALLOW_MACRO
	 * @param int    $rule['length']  (optional)
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateDns($rule, &$data, $path, &$error) {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (self::checkStringUtf8($flags & API_NOT_EMPTY, $data, $path, $error) === false) {
			return false;
		}

		if (($flags & API_NOT_EMPTY) == 0 && $data === '') {
			return true;
		}

		if (array_key_exists('length', $rule) && mb_strlen($data) > $rule['length']) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('value is too long'));
			return false;
		}

		$dns_parser = new CDnsParser([
			'usermacros' => ($flags & API_ALLOW_USER_MACRO),
			'lldmacros' => ($flags & API_ALLOW_LLD_MACRO),
			'macros' => ($flags & API_ALLOW_MACRO)
		]);

		if ($dns_parser->parse($data) != CParser::PARSE_SUCCESS) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('a DNS name is expected'));
			return false;
		}

		return true;
	}

	/**
	 * Port number validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']   (optional) API_NOT_EMPTY, API_ALLOW_USER_MACRO, API_ALLOW_LLD_MACRO
	 * @param int    $rule['length']  (optional)
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validatePort($rule, &$data, $path, &$error) {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (!is_int($data) && !is_string($data)) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('a number is expected'));
			return false;
		}

		$data = (string) $data;

		if (self::checkStringUtf8($flags & API_NOT_EMPTY, $data, $path, $error) === false) {
			return false;
		}

		if (($flags & API_NOT_EMPTY) == 0 && $data === '') {
			return true;
		}

		if (array_key_exists('length', $rule) && mb_strlen($data) > $rule['length']) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('value is too long'));
			return false;
		}

		if ($flags & API_ALLOW_USER_MACRO) {
			$user_macro_parser = new CUserMacroParser();

			if ($user_macro_parser->parse($data) == CParser::PARSE_SUCCESS) {
				return true;
			}
		}

		if ($flags & API_ALLOW_LLD_MACRO) {
			$lld_macro_parser = new CLLDMacroParser();

			if ($lld_macro_parser->parse($data) == CParser::PARSE_SUCCESS) {
				return true;
			}
		}

		if (!self::validateInt32(['in' => ZBX_MIN_PORT_NUMBER.':'.ZBX_MAX_PORT_NUMBER], $data, $path, $error)) {
			return false;
		}

		$data = (string) $data;

		return true;
	}

	/**
	 * Trigger expression validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['length']  (optional)
	 * @param int    $rule['flags']   (optional) API_ALLOW_LLD_MACRO, API_NOT_EMPTY
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateTriggerExpression($rule, &$data, $path, &$error) {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (self::checkStringUtf8($flags & API_NOT_EMPTY, $data, $path, $error) === false) {
			return false;
		}

		if (($flags & API_NOT_EMPTY) == 0 && $data === '') {
			return true;
		}

		if (array_key_exists('length', $rule) && mb_strlen($data) > $rule['length']) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('value is too long'));
			return false;
		}

		$expression_parser = new CExpressionParser([
			'usermacros' => true,
			'lldmacros' => ($flags & API_ALLOW_LLD_MACRO)
		]);

		if ($expression_parser->parse($data) != CParser::PARSE_SUCCESS) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, $expression_parser->getError());
			return false;
		}

		$expression_validator = new CExpressionValidator([
			'usermacros' => true,
			'lldmacros' => ($flags & API_ALLOW_LLD_MACRO)
		]);

		if (!$expression_validator->validate($expression_parser->getResult()->getTokens())) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, $expression_validator->getError());
			return false;
		}

		return true;
	}

	/**
	 * Event name validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['length']  (optional)
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateEventName($rule, &$data, $path, &$error) {
		if (self::checkStringUtf8(0, $data, $path, $error) === false) {
			return false;
		}

		if (array_key_exists('length', $rule) && mb_strlen($data) > $rule['length']) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('value is too long'));
			return false;
		}

		$eventname_validator = new CEventNameValidator();

		if (!$eventname_validator->validate($data)) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, $eventname_validator->getError());
			return false;
		}

		return true;
	}

	/**
	 * JSON RPC parameters validator. Parameters MUST contain an array or object value.
	 *
	 * @param array  $rule
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateJsonRpcParams($rule, &$data, $path, &$error) {
		if (is_array($data)) {
			return true;
		}

		$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('an array or object is expected'));

		return false;
	}

	/**
	 * JSON RPC identifier validator. This identifier MUST contain a String, Number, or NULL value.
	 *
	 * @param array  $rule
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateJsonRpcId($rule, &$data, $path, &$error) {
		if (is_string($data) || is_int($data) || is_float($data) || $data === null) {
			return true;
		}

		$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('a string, number or null value is expected'));

		return false;
	}

	/**
	 * Date validator in YYYY-MM-DD format.
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']  (optional) API_NOT_EMPTY
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateDate(array $rule, &$data, string $path, string &$error): bool {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (self::checkStringUtf8($flags & API_NOT_EMPTY, $data, $path, $error) === false) {
			return false;
		}

		if (($flags & API_NOT_EMPTY) == 0 && $data === '') {
			return true;
		}

		[$year, $month, $day] = sscanf($data, '%d-%d-%d');

		if (!checkdate($month, $day, $year)) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('a date in YYYY-MM-DD format is expected'));
			return false;
		}

		if (!validateDateInterval($year, $month, $day)) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path,
				_s('value must be between "%1$s" and "%2$s"', '1970-01-01', '2038-01-18')
			);
			return false;
		}

		return true;
	}

	/**
	 * Validate numeric ranges. Multiple ranges separated by comma character.
	 * Example:
	 *   10-20,-20--10,-5-0,0.5-0.7,-20--10,-20.20--20.10
	 *   30,-10,0.7,-0.5
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']   (optional) API_NOT_EMPTY
	 * @param int    $rule['length']  (optional)
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateNumericRanges($rule, &$data, $path, &$error) {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (self::checkStringUtf8($flags & API_NOT_EMPTY, $data, $path, $error) === false) {
			return false;
		}

		if (($flags & API_NOT_EMPTY) == 0 && $data === '') {
			return true;
		}

		if (array_key_exists('length', $rule) && mb_strlen($data) > $rule['length']) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('value is too long'));
			return false;
		}

		$parser = new CRangesParser(['with_minus' => true, 'with_float' => true, 'with_suffix' => true]);

		if ($parser->parse($data) != CParser::PARSE_SUCCESS) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('invalid range expression'));

			return false;
		}

		return true;
	}

	/**
	 * UUIDv4 validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']  (optional) API_NOT_EMPTY
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateUuid(array $rule, &$data, string $path, string &$error): bool {
		if (self::checkStringUtf8(API_NOT_EMPTY, $data, $path, $error) === false) {
			return false;
		}

		if (mb_strlen($data) != 32) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _s('must be %1$s characters long', 32));
			return false;
		}

		if (!ctype_xdigit($data)) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('UUIDv4 is expected'));
			return false;
		}

		$binary = hex2bin($data);
		if ((ord($binary[6]) & 0xf0) != 0x40 || (ord($binary[8]) & 0xc0) != 0x80) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('UUIDv4 is expected'));
			return false;
		}

		$data = strtolower($data);

		return true;
	}

	/**
	 * Array of CUIDS validator.
	 *
	 * @param array  $rule
	 * @param int    $rule['flags']  (optional) API_ALLOW_NULL, API_NORMALIZE
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateCuids(array $rule, &$data, string $path, ?string &$error): bool {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (($flags & API_ALLOW_NULL) && $data === null) {
			return true;
		}

		if (($flags & API_NORMALIZE) && self::validateCuid([], $data, '', $e)) {
			$data = [$data];
		}
		unset($e);

		if (!is_array($data)) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('an array is expected'));
			return false;
		}

		$data = array_values($data);

		foreach ($data as $index => &$value) {
			$subpath = ($path === '/' ? $path : $path.'/').($index + 1);
			if (!self::validateCuid([], $value, $subpath, $error)) {
				return false;
			}
		}
		unset($value);

		return true;
	}

	/**
	 * CUID validator.
	 *
	 * @param array  $rule
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateCuid(array $rule, &$data, string $path, ?string &$error): bool {
		if (self::checkStringUtf8(0, $data, $path, $error) === false) {
			return false;
		}

		if (!CCuid::checkLength($data)) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path,
				_s('must be %1$s characters long', CCuid::LENGTH)
			);
			return false;
		}

		if (!CCuid::isCuid($data)) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('CUID is expected'));
			return false;
		}

		return true;
	}

	/**
	 * User vault secret.
	 *
	 * @param array  $rule
	 * @param int    $rule['length']  (optional)
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateVaultSecret($rule, &$data, $path, &$error) {
		if (self::checkStringUtf8(API_NOT_EMPTY, $data, $path, $error) === false) {
			return false;
		}

		if (array_key_exists('length', $rule) && mb_strlen($data) > $rule['length']) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('value is too long'));
			return false;
		}

		$vault_secret_parser = new CVaultSecretParser();

		if ($vault_secret_parser->parse($data) != CParser::PARSE_SUCCESS) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, $vault_secret_parser->getError());
			return false;
		}

		return true;
	}

	/**
	 * Validate image.
	 *
	 * @param array  $rule
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateImage($rule, &$data, $path, &$error) {
		if (self::checkStringUtf8(0, $data, $path, $error) === false) {
			return false;
		}

		$data = base64_decode($data);

		if (bccomp(strlen($data), ZBX_MAX_IMAGE_SIZE) == 1) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path,
				_s('image size must be less than %1$s', convertUnits(['value' => ZBX_MAX_IMAGE_SIZE, 'units' => 'B']))
			);
			return false;
		}

		if (@imageCreateFromString($data) === false) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('file format is unsupported'));
			return false;
		}

		return true;
	}

	/**
	 * @param array  $rule
	 * @param int    $rule['flags']   (optional) API_NOT_EMPTY
	 * @param int    $rule['length']  (optional)
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateExecParams(array $rule, &$data, string $path, string &$error): bool {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (self::checkStringUtf8($flags & API_NOT_EMPTY, $data, $path, $error) === false) {
			return false;
		}

		if (($flags & API_NOT_EMPTY) == 0 && $data === '') {
			return true;
		}

		if (array_key_exists('length', $rule) && mb_strlen($data) > $rule['length']) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('value is too long'));
			return false;
		}

		if ($data !== '' && mb_substr($data, -1) !== "\n") {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('the last new line feed is missing'));
			return false;
		}

		return true;
	}

	/**
	 * Check if input value matches one of following formats:
	 *  - <latitude>,<longitude>,<zoom>
	 *  - <latitude>,<longitude>
	 *
	 * @param array  $rule
	 * @param int    $rule['length']  (optional)
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateLatLngZoom(array $rule, &$data, string $path, string &$error): bool {
		if ($data === '') {
			return true;
		}

		if (array_key_exists('length', $rule) && mb_strlen($data) > $rule['length']) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('value is too long'));
			return false;
		}

		$geoloc_parser = new CGeomapCoordinatesParser();

		if ($geoloc_parser->parse($data) != CParser::PARSE_SUCCESS) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path,
				_('geographical coordinates (values of comma separated latitude and longitude) are expected')
			);
			return false;
		}

		if (array_key_exists('zoom', $geoloc_parser->result) && $geoloc_parser->result['zoom'] > ZBX_GEOMAP_MAX_ZOOM) {
			$error = _s('Invalid zoomparameter "%1$s": %2$s.', $path,
				_s('zoom level must be between "%1$s" and "%2$s"', 0, ZBX_GEOMAP_MAX_ZOOM)
			);
			return false;
		}

		return true;
	}

	/**
	 * Timestamp validator.
	 *
	 * @param array  $rule
	 * @param int    $rule[flags]    (optional) API_ALLOW_NULL
	 * @param string $rule[in]       (optional) A comma-delimited character string, for example: '0,60:900'.
	 * @param array  $rule[compare]  (optional) Data of the object field to compare against.
	 * @param mixed  $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateTimestamp(array $rule, &$data, string $path, string &$error): bool {
		$flags = array_key_exists('flags', $rule) ? $rule['flags'] : 0x00;

		if (($flags & API_ALLOW_NULL) && $data === null) {
			return true;
		}

		if (!is_scalar($data) || is_bool($data) || is_double($data) || !ctype_digit(strval($data))) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('an unsigned integer is expected'));

			return false;
		}

		if (bccomp($data, ZBX_MAX_DATE) > 0) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _('a timestamp is too large'));

			return false;
		}

		if (!self::checkTimestampIn($rule, $data, $path, $error)) {
			return false;
		}

		if (!self::checkCompare($rule, $data, $path, $error)) {
			return false;
		}

		if (is_string($data)) {
			$data = (int) $data;
		}

		return true;
	}

	/**
	 * @param array  $rule
	 * @param string $rule['in']  (optional)
	 * @param int    $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function checkTimestampIn(array $rule, $data, string $path, string &$error): bool {
		if (!array_key_exists('in', $rule)) {
			return true;
		}

		$valid = self::isInRange($data, $rule['in']);

		if (!$valid) {
			$format = array_key_exists('format', $rule) ? $rule['format'] : ZBX_FULL_DATE_TIME;
			$in = explode(',', $rule['in']);
			$formatted_in = '';

			if (array_key_exists('timezone', $rule)) {
				$default_timezone = date_default_timezone_get();
				date_default_timezone_set('UTC');
			}

			foreach ($in as $i => $el) {
				if (strpos($el, ':')) {
					[$from, $to] = explode(':', $el);

					$formatted_in .= date($format, $from).'-'.date($format, $to);
				}
				else {
					$formatted_in .= date($format, $el);
				}

				if (array_key_exists($i + 1, $in)) {
					$formatted_in .= ', ';
				}
			}

			if (array_key_exists('timezone', $rule)) {
				date_default_timezone_set($default_timezone);
			}

			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _n('value must be %1$s', 'value must be one of %1$s',
				$formatted_in, (strpbrk($rule['in'], ',:') === false) ? 1 : 2
			));
		}

		return $valid;
	}

	/**
	 * @param array  $rule
	 * @param array  $rule[compare]            (optional)
	 * @param string $rule[compare][operator]
	 * @param string $rule[compare][path]
	 * @param mixed  $rule[compare][value]
	 * @param int    $data
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function checkCompare(array $rule, $data, string $path, string &$error): bool {
		if (!array_key_exists('compare', $rule)) {
			return true;
		}

		switch ($rule['compare']['operator']) {
			case '>':
				if ($data <= $rule['compare']['value']) {
					$error = _s('Invalid parameter "%1$s": %2$s.', $path,
						_s('cannot be less than or equal to the value of parameter "%1$s"', $rule['compare']['path'])
					);

					return false;
				}
				break;

			default:
				$error = 'Incorrect validation rules.';

				return false;
		}

		return true;
	}

	/**
	 * Unexpected validator.
	 *
	 * @param string $field_name
	 * @param array  $field_rule
	 * @param string $field_rule[error_type]  (optional) API_ERR_INHERITED, API_ERR_DISCOVERED
	 * @param array  $object
	 * @param string $path
	 * @param string $error
	 *
	 * @return bool
	 */
	private static function validateUnexpected(string $field_name, array $field_rule, array $object, string $path,
			string &$error): bool {
		if (!array_key_exists($field_name, $object)) {
			return true;
		}

		if (!array_key_exists('error_type', $field_rule)) {
			$error = _s('Invalid parameter "%1$s": %2$s.', $path, _s('unexpected parameter "%1$s"', $field_name));

			return false;
		}

		switch ($field_rule['error_type']) {
			case API_ERR_INHERITED:
				$error = _s('Invalid parameter "%1$s": %2$s.', $path,
					_s('cannot update readonly parameter "%1$s" of inherited object', $field_name)
				);
				break;

			case API_ERR_DISCOVERED:
				$error = _s('Invalid parameter "%1$s": %2$s.', $path,
					_s('cannot update readonly parameter "%1$s" of discovered object', $field_name)
				);
				break;

			default:
				$error = 'Incorrect validation rules.';
		}

		return false;
	}
}
