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
22/**
23 * Class containing methods for operations with item general.
24 */
25abstract class CItemGeneral extends CApiService {
26
27	public const ACCESS_RULES = [
28		'get' => ['min_user_type' => USER_TYPE_ZABBIX_USER],
29		'create' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN],
30		'update' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN],
31		'delete' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN]
32	];
33
34	const ERROR_EXISTS_TEMPLATE = 'existsTemplate';
35	const ERROR_EXISTS = 'exists';
36	const ERROR_NO_INTERFACE = 'noInterface';
37	const ERROR_INVALID_KEY = 'invalidKey';
38
39	protected $fieldRules;
40
41	/**
42	 * @abstract
43	 *
44	 * @param array $options
45	 *
46	 * @return array
47	 */
48	abstract public function get($options = []);
49
50	public function __construct() {
51		parent::__construct();
52
53		// template - if templated item, value is taken from template item, cannot be changed on host
54		// system - values should not be updated
55		// host - value should be null for template items
56		$this->fieldRules = [
57			'type'					=> ['template' => 1],
58			'snmp_oid'				=> ['template' => 1],
59			'hostid'				=> [],
60			'name'					=> ['template' => 1],
61			'description'			=> [],
62			'key_'					=> ['template' => 1],
63			'master_itemid'			=> ['template' => 1],
64			'delay'					=> [],
65			'history'				=> [],
66			'trends'				=> [],
67			'status'				=> [],
68			'discover'				=> [],
69			'value_type'			=> ['template' => 1],
70			'trapper_hosts'			=> [],
71			'units'					=> ['template' => 1],
72			'formula'				=> ['template' => 1],
73			'error'					=> ['system' => 1],
74			'lastlogsize'			=> ['system' => 1],
75			'logtimefmt'			=> [],
76			'templateid'			=> ['system' => 1],
77			'valuemapid'			=> ['template' => 1],
78			'params'				=> [],
79			'ipmi_sensor'			=> ['template' => 1],
80			'authtype'				=> [],
81			'username'				=> [],
82			'password'				=> [],
83			'publickey'				=> [],
84			'privatekey'			=> [],
85			'mtime'					=> ['system' => 1],
86			'flags'					=> [],
87			'filter'				=> [],
88			'interfaceid'			=> ['host' => 1],
89			'inventory_link'		=> [],
90			'lifetime'				=> [],
91			'preprocessing'			=> ['template' => 1],
92			'overrides'				=> ['template' => 1],
93			'jmx_endpoint'			=> [],
94			'url'					=> ['template' => 1],
95			'timeout'				=> ['template' => 1],
96			'query_fields'			=> ['template' => 1],
97			'parameters'			=> ['template' => 1],
98			'posts'					=> ['template' => 1],
99			'status_codes'			=> ['template' => 1],
100			'follow_redirects'		=> ['template' => 1],
101			'post_type'				=> ['template' => 1],
102			'http_proxy'			=> ['template' => 1],
103			'headers'				=> ['template' => 1],
104			'retrieve_mode'			=> ['template' => 1],
105			'request_method'		=> ['template' => 1],
106			'output_format'			=> ['template' => 1],
107			'allow_traps'			=> [],
108			'ssl_cert_file'			=> ['template' => 1],
109			'ssl_key_file'			=> ['template' => 1],
110			'ssl_key_password'		=> ['template' => 1],
111			'verify_peer'			=> ['template' => 1],
112			'verify_host'			=> ['template' => 1]
113		];
114
115		$this->errorMessages = array_merge($this->errorMessages, [
116			self::ERROR_NO_INTERFACE => _('Cannot find host interface on "%1$s" for item key "%2$s".')
117		]);
118	}
119
120	/**
121	 * Check items data.
122	 *
123	 * Any system field passed to the function will be unset.
124	 *
125	 * @throw APIException
126	 *
127	 * @param array $items passed by reference
128	 * @param bool  $update
129	 */
130	protected function checkInput(array &$items, $update = false) {
131		$api_input_rules = ['type' => API_OBJECT, 'fields' => [
132			'type' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', static::SUPPORTED_ITEM_TYPES)]
133		]];
134		if ($update) {
135			unset($api_input_rules['fields']['type']['flags']);
136		}
137
138		foreach ($items as $num => $item) {
139			$data = array_intersect_key($item, $api_input_rules['fields']);
140			if (!CApiInputValidator::validate($api_input_rules, $data, '/'.($num + 1), $error)) {
141				self::exception(ZBX_API_ERROR_PARAMETERS, $error);
142			}
143		}
144
145		if ($update) {
146			$itemDbFields = ['itemid' => null];
147
148			$dbItemsFields = ['itemid', 'templateid'];
149			foreach ($this->fieldRules as $field => $rule) {
150				if (!isset($rule['system'])) {
151					$dbItemsFields[] = $field;
152				}
153			}
154
155			$dbItems = $this->get([
156				'output' => $dbItemsFields,
157				'itemids' => zbx_objectValues($items, 'itemid'),
158				'editable' => true,
159				'preservekeys' => true
160			]);
161
162			$dbHosts = API::Host()->get([
163				'output' => ['hostid', 'status', 'name'],
164				'hostids' => zbx_objectValues($dbItems, 'hostid'),
165				'templated_hosts' => true,
166				'editable' => true,
167				'preservekeys' => true
168			]);
169		}
170		else {
171			$itemDbFields = [
172				'name' => null,
173				'key_' => null,
174				'hostid' => null,
175				'type' => null,
176				'value_type' => null,
177				'delay' => null
178			];
179
180			$dbHosts = API::Host()->get([
181				'output' => ['hostid', 'status', 'name'],
182				'hostids' => zbx_objectValues($items, 'hostid'),
183				'templated_hosts' => true,
184				'editable' => true,
185				'preservekeys' => true
186			]);
187
188			$discovery_rules = [];
189
190			if ($this instanceof CItemPrototype) {
191				$itemDbFields['ruleid'] = null;
192				$druleids = zbx_objectValues($items, 'ruleid');
193
194				if ($druleids) {
195					$discovery_rules = API::DiscoveryRule()->get([
196						'output' => ['hostid'],
197						'itemids' => $druleids,
198						'preservekeys' => true
199					]);
200				}
201			}
202		}
203
204		// interfaces
205		$interfaces = API::HostInterface()->get([
206			'output' => ['interfaceid', 'hostid', 'type'],
207			'hostids' => zbx_objectValues($dbHosts, 'hostid'),
208			'nopermissions' => true,
209			'preservekeys' => true
210		]);
211
212		if ($update) {
213			$updateDiscoveredValidator = new CUpdateDiscoveredValidator([
214				'allowed' => ['itemid', 'status'],
215				'messageAllowedField' => _('Cannot update "%2$s" for a discovered item "%1$s".')
216			]);
217			foreach ($items as &$item) {
218				// check permissions
219				if (!array_key_exists($item['itemid'], $dbItems)) {
220					self::exception(ZBX_API_ERROR_PERMISSIONS,
221						_('No permissions to referred object or it does not exist!')
222					);
223				}
224
225				$dbItem = $dbItems[$item['itemid']];
226
227				if (array_key_exists('hostid', $item) && bccomp($dbItem['hostid'], $item['hostid']) != 0) {
228					self::exception(ZBX_API_ERROR_PARAMETERS,
229						_s('Incorrect value for field "%1$s": %2$s.', 'hostid', _('cannot be changed'))
230					);
231				}
232
233				$itemName = array_key_exists('name', $item) ? $item['name'] : $dbItem['name'];
234
235				// discovered fields, except status, cannot be updated
236				$updateDiscoveredValidator->setObjectName($itemName);
237				$this->checkPartialValidator($item, $updateDiscoveredValidator, $dbItem);
238
239				$item += [
240					'hostid' => $dbItem['hostid'],
241					'type' => $dbItem['type'],
242					'name' => $dbItem['name'],
243					'key_' => $dbItem['key_'],
244					'flags' => $dbItem['flags']
245				];
246			}
247			unset($item);
248		}
249
250		$item_key_parser = new CItemKey();
251		$ip_range_parser = new CIPRangeParser([
252			'v6' => ZBX_HAVE_IPV6,
253			'ranges' => false,
254			'usermacros' => true,
255			'macros' => [
256				'{HOST.HOST}', '{HOSTNAME}', '{HOST.NAME}', '{HOST.CONN}', '{HOST.IP}', '{IPADDRESS}', '{HOST.DNS}'
257			]
258		]);
259		$update_interval_parser = new CUpdateIntervalParser([
260			'usermacros' => true,
261			'lldmacros' => (get_class($this) === 'CItemPrototype')
262		]);
263
264		$index = 0;
265		foreach ($items as $inum => &$item) {
266			$item = $this->clearValues($item);
267			$index++;
268
269			$fullItem = $items[$inum];
270
271			if (!check_db_fields($itemDbFields, $item)) {
272				self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
273			}
274
275			if ($update) {
276				$type = array_key_exists('type', $item) ? $item['type'] : $dbItems[$item['itemid']]['type'];
277
278				if ($type == ITEM_TYPE_HTTPAGENT) {
279					$this->validateHTTPCheck($fullItem, $dbItems[$item['itemid']]);
280				}
281
282				check_db_fields($dbItems[$item['itemid']], $fullItem);
283
284				$this->checkNoParameters(
285					$item,
286					['templateid', 'state', 'lastlogsize', 'mtime', 'error'],
287					_('Cannot update "%1$s" for item "%2$s".'),
288					$item['name']
289				);
290
291				// apply rules
292				foreach ($this->fieldRules as $field => $rules) {
293					if ($fullItem['type'] == ITEM_TYPE_SCRIPT) {
294						$rules['template'] = 1;
295					}
296
297					if ((0 != $fullItem['templateid'] && isset($rules['template'])) || isset($rules['system'])) {
298						unset($item[$field]);
299
300						// For templated item and fields that should not be modified, use the value from DB.
301						if (array_key_exists($field, $dbItems[$item['itemid']])
302								&& array_key_exists($field, $fullItem)) {
303							$fullItem[$field] = $dbItems[$item['itemid']][$field];
304						}
305					}
306				}
307
308				if (!isset($item['key_'])) {
309					$item['key_'] = $fullItem['key_'];
310				}
311				if (!isset($item['hostid'])) {
312					$item['hostid'] = $fullItem['hostid'];
313				}
314
315				// if a templated item is being assigned to an interface with a different type, ignore it
316				$itemInterfaceType = itemTypeInterface($dbItems[$item['itemid']]['type']);
317				if ($fullItem['templateid'] && isset($item['interfaceid']) && isset($interfaces[$item['interfaceid']])
318						&& $itemInterfaceType !== INTERFACE_TYPE_ANY && $interfaces[$item['interfaceid']]['type'] != $itemInterfaceType) {
319
320					unset($item['interfaceid']);
321				}
322			}
323			else {
324				if ($fullItem['type'] == ITEM_TYPE_HTTPAGENT) {
325					$this->validateHTTPCheck($fullItem, []);
326				}
327
328				if (!isset($dbHosts[$item['hostid']])) {
329					self::exception(ZBX_API_ERROR_PARAMETERS, _('No permissions to referred object or it does not exist!'));
330				}
331
332				check_db_fields($itemDbFields, $fullItem);
333
334				$this->checkNoParameters(
335					$item,
336					['templateid', 'state'],
337					_('Cannot set "%1$s" for item "%2$s".'),
338					$item['name']
339				);
340
341				if ($this instanceof CItemPrototype && (!array_key_exists($fullItem['ruleid'], $discovery_rules)
342						|| $discovery_rules[$fullItem['ruleid']]['hostid'] != $fullItem['hostid'])) {
343					self::exception(ZBX_API_ERROR_PARAMETERS,
344						_('No permissions to referred object or it does not exist!')
345					);
346				}
347			}
348
349			if ($fullItem['type'] == ITEM_TYPE_CALCULATED) {
350				$api_input_rules = ['type' => API_OBJECT, 'fields' => [
351					'params' =>		['type' => API_CALC_FORMULA, 'flags' => $this instanceof CItemPrototype ? API_ALLOW_LLD_MACRO : 0, 'length' => DB::getFieldLength('items', 'params')],
352					'value_type' =>	['type' => API_INT32, 'in' => ITEM_VALUE_TYPE_UINT64.','.ITEM_VALUE_TYPE_FLOAT]
353				]];
354
355				$data = array_intersect_key($item, $api_input_rules['fields']);
356
357				if (!CApiInputValidator::validate($api_input_rules, $data, '/'.($inum + 1), $error)) {
358					self::exception(ZBX_API_ERROR_PARAMETERS, $error);
359				}
360			}
361
362			if ($fullItem['type'] == ITEM_TYPE_SCRIPT) {
363				if ($update) {
364					if ($dbItems[$item['itemid']]['type'] == $fullItem['type']) {
365						$flags = API_NOT_EMPTY;
366					}
367					else {
368						$flags = API_REQUIRED | API_NOT_EMPTY;
369					}
370				}
371				else {
372					$flags = API_REQUIRED | API_NOT_EMPTY;
373				}
374
375				$api_input_rules = ['type' => API_OBJECT, 'fields' => [
376					'params' => ['type' => API_STRING_UTF8, 'flags' => $flags, 'length' => DB::getFieldLength('items', 'params')],
377					'timeout' => [
378						'type' => API_TIME_UNIT, 'flags' => ($this instanceof CItemPrototype)
379							? $flags | API_ALLOW_USER_MACRO | API_ALLOW_LLD_MACRO
380							: $flags | API_ALLOW_USER_MACRO,
381						'in' => '1:'.SEC_PER_MIN
382					],
383					'parameters' => ['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['name']], 'fields' => [
384						'name' =>		['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('item_parameter', 'name')],
385						'value' =>		['type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('item_parameter', 'value')]
386					]]
387				]];
388
389				$data = array_intersect_key($item, $api_input_rules['fields']);
390
391				if (!CApiInputValidator::validate($api_input_rules, $data, '/'.($inum + 1), $error)) {
392					self::exception(ZBX_API_ERROR_PARAMETERS, $error);
393				}
394			}
395
396			$host = $dbHosts[$fullItem['hostid']];
397
398			// Validate update interval.
399			if (!in_array($fullItem['type'], [ITEM_TYPE_TRAPPER, ITEM_TYPE_SNMPTRAP, ITEM_TYPE_DEPENDENT])
400					&& ($fullItem['type'] != ITEM_TYPE_ZABBIX_ACTIVE || strncmp($fullItem['key_'], 'mqtt.get', 8) !== 0)
401					&& !validateDelay($update_interval_parser, 'delay', $fullItem['delay'], $error)) {
402				self::exception(ZBX_API_ERROR_PARAMETERS, $error);
403			}
404
405			// For non-numeric types, whichever value was entered in trends field, is overwritten to zero.
406			if ($fullItem['value_type'] == ITEM_VALUE_TYPE_STR || $fullItem['value_type'] == ITEM_VALUE_TYPE_LOG
407					|| $fullItem['value_type'] == ITEM_VALUE_TYPE_TEXT) {
408				$item['trends'] = '0';
409			}
410
411			// check if the item requires an interface
412			if ($host['status'] == HOST_STATUS_TEMPLATE) {
413				unset($item['interfaceid']);
414			}
415			else {
416				$itemInterfaceType = itemTypeInterface($fullItem['type']);
417
418				if ($itemInterfaceType !== false) {
419					if (!array_key_exists('interfaceid', $fullItem) || !$fullItem['interfaceid']) {
420						self::exception(ZBX_API_ERROR_PARAMETERS, _('No interface found.'));
421					}
422					elseif (!isset($interfaces[$fullItem['interfaceid']]) || bccomp($interfaces[$fullItem['interfaceid']]['hostid'], $fullItem['hostid']) != 0) {
423						self::exception(ZBX_API_ERROR_PARAMETERS, _('Item uses host interface from non-parent host.'));
424					}
425					elseif ($itemInterfaceType !== INTERFACE_TYPE_ANY && $interfaces[$fullItem['interfaceid']]['type'] != $itemInterfaceType) {
426						self::exception(ZBX_API_ERROR_PARAMETERS, _('Item uses incorrect interface type.'));
427					}
428				}
429				// no interface required, just set it to null
430				else {
431					$item['interfaceid'] = 0;
432				}
433			}
434
435			// item key
436			if ($fullItem['type'] == ITEM_TYPE_DB_MONITOR) {
437				if (!isset($fullItem['flags']) || $fullItem['flags'] != ZBX_FLAG_DISCOVERY_RULE) {
438					if (strcmp($fullItem['key_'], ZBX_DEFAULT_KEY_DB_MONITOR) == 0) {
439						self::exception(ZBX_API_ERROR_PARAMETERS,
440							_('Check the key, please. Default example was passed.')
441						);
442					}
443				}
444				elseif ($fullItem['flags'] == ZBX_FLAG_DISCOVERY_RULE) {
445					if (strcmp($fullItem['key_'], ZBX_DEFAULT_KEY_DB_MONITOR_DISCOVERY) == 0) {
446						self::exception(ZBX_API_ERROR_PARAMETERS,
447							_('Check the key, please. Default example was passed.')
448						);
449					}
450				}
451			}
452			elseif (($fullItem['type'] == ITEM_TYPE_SSH && strcmp($fullItem['key_'], ZBX_DEFAULT_KEY_SSH) == 0)
453					|| ($fullItem['type'] == ITEM_TYPE_TELNET && strcmp($fullItem['key_'], ZBX_DEFAULT_KEY_TELNET) == 0)) {
454				self::exception(ZBX_API_ERROR_PARAMETERS, _('Check the key, please. Default example was passed.'));
455			}
456
457			// key
458			if ($item_key_parser->parse($fullItem['key_']) != CParser::PARSE_SUCCESS) {
459				self::exception(ZBX_API_ERROR_PARAMETERS,
460					_params($this->getErrorMsg(self::ERROR_INVALID_KEY), [
461						$fullItem['key_'], $fullItem['name'], $host['name'], $item_key_parser->getError()
462					])
463				);
464			}
465
466			if (($fullItem['type'] == ITEM_TYPE_TRAPPER || $fullItem['type'] == ITEM_TYPE_HTTPAGENT)
467					&& array_key_exists('trapper_hosts', $fullItem) && $fullItem['trapper_hosts'] !== ''
468					&& !$ip_range_parser->parse($fullItem['trapper_hosts'])) {
469				self::exception(ZBX_API_ERROR_PARAMETERS,
470					_s('Incorrect value for field "%1$s": %2$s.', 'trapper_hosts', $ip_range_parser->getError())
471				);
472			}
473
474			// jmx
475			if ($fullItem['type'] == ITEM_TYPE_JMX) {
476				if (!array_key_exists('jmx_endpoint', $fullItem) && !$update) {
477					$item['jmx_endpoint'] = ZBX_DEFAULT_JMX_ENDPOINT;
478				}
479				if (array_key_exists('jmx_endpoint', $fullItem) && $fullItem['jmx_endpoint'] === '') {
480					self::exception(ZBX_API_ERROR_PARAMETERS,
481						_s('Incorrect value for field "%1$s": %2$s.', 'jmx_endpoint', _('cannot be empty'))
482					);
483				}
484
485				if (($fullItem['username'] === '') !== ($fullItem['password'] === '')) {
486					self::exception(ZBX_API_ERROR_PARAMETERS,
487						_s('Incorrect value for field "%1$s": %2$s.', 'username',
488								_('both username and password should be either present or empty'))
489					);
490				}
491			}
492			else {
493				if (array_key_exists('jmx_endpoint', $item) && $item['jmx_endpoint'] !== '') {
494					self::exception(ZBX_API_ERROR_PARAMETERS,
495						_s('Incorrect value for field "%1$s": %2$s.', 'jmx_endpoint', _('should be empty'))
496					);
497				}
498				elseif (array_key_exists('jmx_endpoint', $fullItem) && $fullItem['jmx_endpoint'] !== '') {
499					$item['jmx_endpoint'] = '';
500				}
501			}
502
503			// Dependent item.
504			if ($fullItem['type'] == ITEM_TYPE_DEPENDENT) {
505				if ($update) {
506					if (array_key_exists('master_itemid', $item) && !$item['master_itemid']) {
507						self::exception(ZBX_API_ERROR_PERMISSIONS, _s('Incorrect value for field "%1$s": %2$s.',
508							'master_itemid', _('cannot be empty')
509						));
510					}
511					if ($dbItems[$fullItem['itemid']]['type'] != ITEM_TYPE_DEPENDENT
512							&& !array_key_exists('master_itemid', $item)) {
513						self::exception(ZBX_API_ERROR_PERMISSIONS, _s('Incorrect value for field "%1$s": %2$s.',
514							'master_itemid', _('cannot be empty')
515						));
516					}
517				}
518				elseif (!array_key_exists('master_itemid', $item) || !$item['master_itemid']) {
519					self::exception(ZBX_API_ERROR_PERMISSIONS, _s('Incorrect value for field "%1$s": %2$s.',
520						'master_itemid', _('cannot be empty')
521					));
522				}
523				if (array_key_exists('master_itemid', $item) && !is_int($item['master_itemid'])
524						&& !(is_string($item['master_itemid']) && ctype_digit($item['master_itemid']))) {
525					self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value "%1$s" for "%2$s" field.',
526						$item['master_itemid'], 'master_itemid'
527					));
528				}
529			}
530			else {
531				if (array_key_exists('master_itemid', $item) && $item['master_itemid']) {
532					self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
533						'master_itemid', _('should be empty')
534					));
535				}
536				$item['master_itemid'] = 0;
537			}
538
539			// ssh, telnet
540			if ($fullItem['type'] == ITEM_TYPE_SSH || $fullItem['type'] == ITEM_TYPE_TELNET) {
541				if ($fullItem['username'] === '') {
542					self::exception(ZBX_API_ERROR_PARAMETERS, _('No authentication user name specified.'));
543				}
544
545				if ($fullItem['type'] == ITEM_TYPE_SSH && $fullItem['authtype'] == ITEM_AUTHTYPE_PUBLICKEY) {
546					if ($fullItem['publickey'] === '') {
547						self::exception(ZBX_API_ERROR_PARAMETERS, _('No public key file specified.'));
548					}
549					if ($fullItem['privatekey'] === '') {
550						self::exception(ZBX_API_ERROR_PARAMETERS, _('No private key file specified.'));
551					}
552				}
553			}
554
555			// Prevent IPMI sensor field being empty if item key is not "ipmi.get".
556			if ($fullItem['type'] == ITEM_TYPE_IPMI && $fullItem['key_'] !== 'ipmi.get'
557					&& (!array_key_exists('ipmi_sensor', $fullItem) || $fullItem['ipmi_sensor'] === '')) {
558				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
559					'ipmi_sensor', _('cannot be empty')
560				));
561			}
562
563			// snmp trap
564			if ($fullItem['type'] == ITEM_TYPE_SNMPTRAP
565					&& $fullItem['key_'] !== 'snmptrap.fallback' && $item_key_parser->getKey() !== 'snmptrap') {
566				self::exception(ZBX_API_ERROR_PARAMETERS, _('SNMP trap key is invalid.'));
567			}
568
569			// snmp oid
570			if ($fullItem['type'] == ITEM_TYPE_SNMP
571					&& (!array_key_exists('snmp_oid', $fullItem) || $fullItem['snmp_oid'] === '')) {
572				self::exception(ZBX_API_ERROR_PARAMETERS, _('No SNMP OID specified.'));
573			}
574
575			$this->checkSpecificFields($fullItem, $update ? 'update' : 'create');
576
577			$this->validateItemPreprocessing($fullItem);
578			$this->validateTags($item, '/'.$index);
579		}
580		unset($item);
581
582		$this->validateValueMaps($items);
583
584		$this->checkAndAddUuid($items, $dbHosts, $update);
585		$this->checkExistingItems($items);
586	}
587
588	/**
589	 * Check that only items on templates have UUID. Add UUID to all host prototypes on templates,
590	 *   if it doesn't exist.
591	 *
592	 * @param array $items_to_create
593	 * @param array $db_hosts
594	 * @param bool $is_update
595	 *
596	 * @throws APIException
597	 */
598	protected function checkAndAddUuid(array &$items_to_create, array $db_hosts, bool $is_update): void {
599		if ($is_update) {
600			foreach ($items_to_create as $index => &$item) {
601				if (array_key_exists('uuid', $item)) {
602					self::exception(ZBX_API_ERROR_PARAMETERS,
603						_s('Invalid parameter "%1$s": %2$s.', '/' . ($index + 1),
604							_s('unexpected parameter "%1$s"', 'uuid')
605						)
606					);
607				}
608			}
609
610			return;
611		}
612
613		foreach ($items_to_create as $index => &$item) {
614			if ($db_hosts[$item['hostid']]['status'] != HOST_STATUS_TEMPLATE && array_key_exists('uuid', $item)) {
615				self::exception(ZBX_API_ERROR_PARAMETERS,
616					_s('Invalid parameter "%1$s": %2$s.', '/' . ($index + 1), _s('unexpected parameter "%1$s"', 'uuid'))
617				);
618			}
619
620			if ($db_hosts[$item['hostid']]['status'] == HOST_STATUS_TEMPLATE && !array_key_exists('uuid', $item)) {
621				$item['uuid'] = generateUuidV4();
622			}
623		}
624		unset($item);
625
626		$db_uuid = DB::select('items', [
627			'output' => ['uuid'],
628			'filter' => ['uuid' => array_column($items_to_create, 'uuid')],
629			'limit' => 1
630		]);
631
632		if ($db_uuid) {
633			self::exception(ZBX_API_ERROR_PARAMETERS,
634				_s('Entry with UUID "%1$s" already exists.', $db_uuid[0]['uuid'])
635			);
636		}
637	}
638
639	/**
640	 * Validates tags.
641	 *
642	 * @param array  $item
643	 * @param array  $item['tags']
644	 * @param string $item['tags'][]['tag']
645	 * @param string $item['tags'][]['value']
646	 *
647	 * @throws APIException if the input is invalid.
648	 */
649	protected function validateTags(array $item, string $path = '/') {
650		if (!array_key_exists('tags', $item)) {
651			return;
652		}
653
654		$api_input_rules = ['type' => API_OBJECT, 'fields' => [
655			'tags'		=> ['type' => API_OBJECTS, 'uniq' => [['tag', 'value']], 'fields' => [
656				'tag'		=> ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('item_tag', 'tag')],
657				'value'		=> ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('item_tag', 'value')]
658			]]
659		]];
660
661		$item_tags = ['tags' => $item['tags']];
662		if (!CApiInputValidator::validate($api_input_rules, $item_tags, $path, $error)) {
663			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
664		}
665	}
666
667	/**
668	 * Check item specific fields. Each API like Item, Itemprototype and Discovery rule may inherit different fields
669	 * to validate.
670	 *
671	 * @param array  $item    An array of single item data.
672	 * @param string $method  A string of "create" or "update" method.
673	 *
674	 * @return bool
675	 */
676	abstract protected function checkSpecificFields(array $item, $method);
677
678	protected function clearValues(array $item) {
679		if (isset($item['port']) && $item['port'] != '') {
680			$item['port'] = ltrim($item['port'], '0');
681			if ($item['port'] == '') {
682				$item['port'] = 0;
683			}
684		}
685
686		if (array_key_exists('type', $item) &&
687				($item['type'] == ITEM_TYPE_DEPENDENT || $item['type'] == ITEM_TYPE_TRAPPER
688					|| ($item['type'] == ITEM_TYPE_ZABBIX_ACTIVE && array_key_exists('key_', $item)
689						&& strncmp($item['key_'], 'mqtt.get', 8) === 0))) {
690			$item['delay'] = 0;
691		}
692
693		return $item;
694	}
695
696	protected function errorInheritFlags($flag, $key, $host) {
697		switch ($flag) {
698			case ZBX_FLAG_DISCOVERY_NORMAL:
699				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Item with key "%1$s" already exists on "%2$s" as an item.', $key, $host));
700				break;
701			case ZBX_FLAG_DISCOVERY_RULE:
702				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Item with key "%1$s" already exists on "%2$s" as a discovery rule.', $key, $host));
703				break;
704			case ZBX_FLAG_DISCOVERY_PROTOTYPE:
705				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Item with key "%1$s" already exists on "%2$s" as an item prototype.', $key, $host));
706				break;
707			case ZBX_FLAG_DISCOVERY_CREATED:
708				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Item with key "%1$s" already exists on "%2$s" as an item created from item prototype.', $key, $host));
709				break;
710			default:
711				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Item with key "%1$s" already exists on "%2$s" as unknown item element.', $key, $host));
712		}
713	}
714
715	/**
716	 * Returns the interface that best matches the given item.
717	 *
718	 * @param array $item_type  An item type
719	 * @param array $interfaces An array of interfaces to choose from
720	 *
721	 * @return array|boolean    The best matching interface;
722	 *							an empty array of no matching interface was found;
723	 *							false, if the item does not need an interface
724	 */
725	public static function findInterfaceForItem($item_type, array $interfaces) {
726		$interface_by_type = [];
727		foreach ($interfaces as $interface) {
728			if ($interface['main'] == 1) {
729				$interface_by_type[$interface['type']] = $interface;
730			}
731		}
732
733		// find item interface type
734		$type = itemTypeInterface($item_type);
735
736		// the item can use any interface
737		if ($type == INTERFACE_TYPE_ANY) {
738			$interface_types = [INTERFACE_TYPE_AGENT, INTERFACE_TYPE_SNMP, INTERFACE_TYPE_JMX, INTERFACE_TYPE_IPMI];
739			foreach ($interface_types as $interface_type) {
740				if (array_key_exists($interface_type, $interface_by_type)) {
741					return $interface_by_type[$interface_type];
742				}
743			}
744		}
745		// the item uses a specific type of interface
746		elseif ($type !== false) {
747			return array_key_exists($type, $interface_by_type) ? $interface_by_type[$type] : [];
748		}
749		// the item does not need an interface
750		else {
751			return false;
752		}
753	}
754
755	/**
756	 * Updates the children of the item on the given hosts and propagates the inheritance to the child hosts.
757	 *
758	 * @param array      $tpl_items  An array of items to inherit.
759	 * @param array|null $hostids    An array of hosts to inherit to; if set to null, the items will be inherited to all
760	 *                               linked hosts or templates.
761	 */
762	protected function inherit(array $tpl_items, array $hostids = null) {
763		$tpl_items = zbx_toHash($tpl_items, 'itemid');
764
765		// Inherit starting from common items and finishing up dependent.
766		while ($tpl_items) {
767			$_tpl_items = [];
768
769			foreach ($tpl_items as $tpl_item) {
770				if ($tpl_item['type'] != ITEM_TYPE_DEPENDENT
771						|| !array_key_exists($tpl_item['master_itemid'], $tpl_items)) {
772					$_tpl_items[$tpl_item['itemid']] = $tpl_item;
773				}
774			}
775
776			foreach ($_tpl_items as $itemid => $_tpl_item) {
777				unset($tpl_items[$itemid]);
778			}
779
780			$this->_inherit($_tpl_items, $hostids);
781		}
782	}
783
784	/**
785	 * Auxiliary method for item inheritance. See full description in inherit() method.
786	 */
787	private function _inherit(array $tpl_items, array $hostids = null) {
788		// Prepare the child items.
789		$new_items = $this->prepareInheritedItems($tpl_items, $hostids);
790		if (!$new_items) {
791			return;
792		}
793
794		$ins_items = [];
795		$upd_items = [];
796
797		foreach ($new_items as $new_item) {
798			if (array_key_exists('itemid', $new_item)) {
799				if ($this instanceof CItemPrototype) {
800					unset($new_item['ruleid']);
801				}
802				$upd_items[$new_item['itemid']] = $new_item;
803			}
804			else {
805				$ins_items[] = $new_item;
806			}
807		}
808
809		$this->validateDependentItems($new_items);
810
811		// Save the new items.
812		if ($ins_items) {
813			if ($this instanceof CItem) {
814				static::validateInventoryLinks($ins_items, false);
815			}
816
817			$this->createReal($ins_items);
818		}
819
820		if ($upd_items) {
821			if ($this instanceof CItem) {
822				static::validateInventoryLinks($upd_items, true);
823			}
824
825			$this->updateReal($upd_items);
826		}
827
828		$new_items = array_merge($upd_items, $ins_items);
829
830		// Inheriting items from the templates.
831		$db_items = DBselect(
832			'SELECT i.itemid'.
833			' FROM items i,hosts h'.
834			' WHERE i.hostid=h.hostid'.
835				' AND '.dbConditionInt('i.itemid', zbx_objectValues($new_items, 'itemid')).
836				' AND '.dbConditionInt('h.status', [HOST_STATUS_TEMPLATE])
837		);
838
839		$tpl_itemids = [];
840		while ($db_item = DBfetch($db_items)) {
841			$tpl_itemids[$db_item['itemid']] = true;
842		}
843
844		foreach ($new_items as $index => $new_item) {
845			if (!array_key_exists($new_item['itemid'], $tpl_itemids)) {
846				unset($new_items[$index]);
847			}
848		}
849
850		$this->inherit($new_items);
851	}
852
853	/**
854	 * Prepares and returns an array of child items, inherited from items $tpl_items on the given hosts.
855	 *
856	 * @param array      $tpl_items
857	 * @param string     $tpl_items[<itemid>]['itemid']
858	 * @param string     $tpl_items[<itemid>]['hostid']
859	 * @param string     $tpl_items[<itemid>]['key_']
860	 * @param int        $tpl_items[<itemid>]['type']
861	 * @param array      $tpl_items[<itemid>]['preprocessing']                    (optional)
862	 * @param int        $tpl_items[<itemid>]['preprocessing'][]['type']
863	 * @param string     $tpl_items[<itemid>]['preprocessing'][]['params']
864	 * @param int        $tpl_items[<itemid>]['flags']
865	 * @param string     $tpl_items[<itemid>]['master_itemid']                    (optional)
866	 * @param mixed      $tpl_items[<itemid>][<field_name>]                       (optional)
867	 * @param array|null $hostids
868	 *
869	 * @return array an array of unsaved child items
870	 */
871	private function prepareInheritedItems(array $tpl_items, array $hostids = null) {
872		$itemids_by_templateid = [];
873		foreach ($tpl_items as $tpl_item) {
874			$itemids_by_templateid[$tpl_item['hostid']][] = $tpl_item['itemid'];
875		}
876
877		// Fetch all child hosts.
878		$chd_hosts = API::Host()->get([
879			'output' => ['hostid', 'host', 'status'],
880			'selectParentTemplates' => ['templateid'],
881			'selectInterfaces' => ['interfaceid', 'main', 'type'],
882			'templateids' => array_keys($itemids_by_templateid),
883			'hostids' => $hostids,
884			'preservekeys' => true,
885			'nopermissions' => true,
886			'templated_hosts' => true
887		]);
888		if (!$chd_hosts) {
889			return [];
890		}
891
892		$chd_items_tpl = [];
893		$chd_items_key = [];
894
895		// Preparing list of items by item templateid.
896		$sql = 'SELECT i.itemid,i.hostid,i.type,i.key_,i.flags,i.templateid'.
897			' FROM items i'.
898			' WHERE '.dbConditionInt('i.templateid', zbx_objectValues($tpl_items, 'itemid'));
899		if ($hostids !== null) {
900			$sql .= ' AND '.dbConditionInt('i.hostid', $hostids);
901		}
902		$db_items = DBselect($sql);
903
904		while ($db_item = DBfetch($db_items)) {
905			$hostid = $db_item['hostid'];
906			unset($db_item['hostid']);
907
908			$chd_items_tpl[$hostid][$db_item['templateid']] = $db_item;
909		}
910
911		$hostids_by_key = [];
912
913		// Preparing list of items by item key.
914		foreach ($chd_hosts as $chd_host) {
915			$tpl_itemids = [];
916
917			foreach ($chd_host['parentTemplates'] as $parent_template) {
918				if (array_key_exists($parent_template['templateid'], $itemids_by_templateid)) {
919					$tpl_itemids = array_merge($tpl_itemids, $itemids_by_templateid[$parent_template['templateid']]);
920				}
921			}
922
923			foreach ($tpl_itemids as $tpl_itemid) {
924				if (!array_key_exists($chd_host['hostid'], $chd_items_tpl)
925						|| !array_key_exists($tpl_itemid, $chd_items_tpl[$chd_host['hostid']])) {
926					$hostids_by_key[$tpl_items[$tpl_itemid]['key_']][] = $chd_host['hostid'];
927				}
928			}
929		}
930
931		foreach ($hostids_by_key as $key_ => $key_hostids) {
932			$sql_select = ($this instanceof CItemPrototype) ? ',id.parent_itemid AS ruleid' : '';
933			// "LEFT JOIN" is needed to check flags on inherited and existing item, item prototype or lld rule.
934			// For example, when linking an item prototype with same key as in an item on target host or template.
935			$sql_join = ($this instanceof CItemPrototype) ? ' LEFT JOIN item_discovery id ON i.itemid=id.itemid' : '';
936			$db_items = DBselect(
937				'SELECT i.itemid,i.hostid,i.type,i.key_,i.flags,i.templateid'.$sql_select.
938					' FROM items i'.$sql_join.
939					' WHERE '.dbConditionInt('i.hostid', $key_hostids).
940						' AND '.dbConditionString('i.key_', [$key_])
941			);
942
943			while ($db_item = DBfetch($db_items)) {
944				$hostid = $db_item['hostid'];
945				unset($db_item['hostid']);
946
947				$chd_items_key[$hostid][$db_item['key_']] = $db_item;
948			}
949		}
950
951		// List of the discovery rules.
952		if ($this instanceof CItemPrototype) {
953			// List of itemids without 'ruleid' property.
954			$tpl_itemids = [];
955			$tpl_ruleids = [];
956			foreach ($tpl_items as $tpl_item) {
957				if (!array_key_exists('ruleid', $tpl_item)) {
958					$tpl_itemids[] = $tpl_item['itemid'];
959				}
960				else {
961					$tpl_ruleids[$tpl_item['ruleid']] = true;
962				}
963			}
964
965			if ($tpl_itemids) {
966				$db_rules = DBselect(
967					'SELECT id.parent_itemid,id.itemid'.
968						' FROM item_discovery id'.
969						' WHERE '.dbConditionInt('id.itemid', $tpl_itemids)
970				);
971
972				while ($db_rule = DBfetch($db_rules)) {
973					$tpl_items[$db_rule['itemid']]['ruleid'] = $db_rule['parent_itemid'];
974					$tpl_ruleids[$db_rule['parent_itemid']] = true;
975				}
976			}
977
978			$sql = 'SELECT i.hostid,i.templateid,i.itemid'.
979					' FROM items i'.
980					' WHERE '.dbConditionInt('i.templateid', array_keys($tpl_ruleids));
981			if ($hostids !== null) {
982				$sql .= ' AND '.dbConditionInt('i.hostid', $hostids);
983			}
984			$db_rules = DBselect($sql);
985
986			// List of child lld ruleids by child hostid and parent lld ruleid.
987			$chd_ruleids = [];
988			while ($db_rule = DBfetch($db_rules)) {
989				$chd_ruleids[$db_rule['hostid']][$db_rule['templateid']] = $db_rule['itemid'];
990			}
991		}
992
993		$new_items = [];
994		// List of the updated item keys by hostid.
995		$upd_hostids_by_key = [];
996
997		foreach ($chd_hosts as $chd_host) {
998			$tpl_itemids = [];
999
1000			foreach ($chd_host['parentTemplates'] as $parent_template) {
1001				if (array_key_exists($parent_template['templateid'], $itemids_by_templateid)) {
1002					$tpl_itemids = array_merge($tpl_itemids, $itemids_by_templateid[$parent_template['templateid']]);
1003				}
1004			}
1005
1006			foreach ($tpl_itemids as $tpl_itemid) {
1007				$tpl_item = $tpl_items[$tpl_itemid];
1008
1009				$chd_item = null;
1010
1011				// Update by templateid.
1012				if (array_key_exists($chd_host['hostid'], $chd_items_tpl)
1013						&& array_key_exists($tpl_item['itemid'], $chd_items_tpl[$chd_host['hostid']])) {
1014					$chd_item = $chd_items_tpl[$chd_host['hostid']][$tpl_item['itemid']];
1015
1016					if ($tpl_item['key_'] !== $chd_item['key_']) {
1017						$upd_hostids_by_key[$tpl_item['key_']][] = $chd_host['hostid'];
1018					}
1019				}
1020				// Update by key.
1021				elseif (array_key_exists($chd_host['hostid'], $chd_items_key)
1022						&& array_key_exists($tpl_item['key_'], $chd_items_key[$chd_host['hostid']])) {
1023					$chd_item = $chd_items_key[$chd_host['hostid']][$tpl_item['key_']];
1024
1025					// Check if an item of a different type with the same key exists.
1026					if ($tpl_item['flags'] != $chd_item['flags']) {
1027						$this->errorInheritFlags($chd_item['flags'], $chd_item['key_'], $chd_host['host']);
1028					}
1029
1030					// Check if item already linked to another template.
1031					if ($chd_item['templateid'] != 0 && bccomp($chd_item['templateid'], $tpl_item['itemid']) != 0) {
1032						self::exception(ZBX_API_ERROR_PARAMETERS, _params(
1033							$this->getErrorMsg(self::ERROR_EXISTS_TEMPLATE), [$tpl_item['key_'], $chd_host['host']]
1034						));
1035					}
1036
1037					if ($this instanceof CItemPrototype) {
1038						$chd_ruleid = $chd_ruleids[$chd_host['hostid']][$tpl_item['ruleid']];
1039						if (bccomp($chd_item['ruleid'], $chd_ruleid) != 0) {
1040							self::exception(ZBX_API_ERROR_PARAMETERS,
1041								_s('Item prototype "%1$s" already exists on "%2$s", linked to another rule.',
1042									$chd_item['key_'], $chd_host['host']
1043								)
1044							);
1045						}
1046					}
1047				}
1048
1049				// copying item
1050				$new_item = $tpl_item;
1051				$new_item['uuid'] = '';
1052
1053				if ($chd_item !== null) {
1054					$new_item['itemid'] = $chd_item['itemid'];
1055				}
1056				else {
1057					unset($new_item['itemid']);
1058					if ($this instanceof CItemPrototype) {
1059						$new_item['ruleid'] = $chd_ruleids[$chd_host['hostid']][$tpl_item['ruleid']];
1060					}
1061				}
1062				$new_item['hostid'] = $chd_host['hostid'];
1063				$new_item['templateid'] = $tpl_item['itemid'];
1064
1065				if ($chd_host['status'] != HOST_STATUS_TEMPLATE) {
1066					if ($chd_item === null || $new_item['type'] != $chd_item['type']) {
1067						$interface = self::findInterfaceForItem($new_item['type'], $chd_host['interfaces']);
1068
1069						if ($interface) {
1070							$new_item['interfaceid'] = $interface['interfaceid'];
1071						}
1072						elseif ($interface !== false) {
1073							self::exception(ZBX_API_ERROR_PARAMETERS, _params(
1074								$this->getErrorMsg(self::ERROR_NO_INTERFACE), [$chd_host['host'], $new_item['key_']]
1075							));
1076						}
1077					}
1078
1079					if ($this instanceof CItem || $this instanceof CDiscoveryRule) {
1080						if (!array_key_exists('itemid', $new_item)) {
1081							$new_item['rtdata'] = true;
1082						}
1083					}
1084				}
1085
1086				if (array_key_exists('preprocessing', $new_item)) {
1087					foreach ($new_item['preprocessing'] as $preprocessing) {
1088						if ($chd_item) {
1089							$preprocessing['itemid'] = $chd_item['itemid'];
1090						}
1091						else {
1092							unset($preprocessing['itemid']);
1093						}
1094					}
1095				}
1096
1097				$new_items[] = $new_item;
1098			}
1099		}
1100
1101		// Check if item with a new key already exists on the child host.
1102		if ($upd_hostids_by_key) {
1103			$sql_where = [];
1104			foreach ($upd_hostids_by_key as $key => $hostids) {
1105				$sql_where[] = dbConditionInt('i.hostid', $hostids).' AND i.key_='.zbx_dbstr($key);
1106			}
1107
1108			$sql = 'SELECT i.hostid,i.key_'.
1109				' FROM items i'.
1110				' WHERE ('.implode(') OR (', $sql_where).')';
1111			$db_items = DBselect($sql, 1);
1112
1113			if ($db_item = DBfetch($db_items)) {
1114				self::exception(ZBX_API_ERROR_PARAMETERS, _params($this->getErrorMsg(self::ERROR_EXISTS),
1115					[$db_item['key_'], $chd_hosts[$db_item['hostid']]['host']]
1116				));
1117			}
1118		}
1119
1120		return $this->prepareDependentItems($tpl_items, $new_items, $hostids);
1121	}
1122
1123	/**
1124	 * Update relations for inherited dependent items to master items.
1125	 *
1126	 * @param array      $tpl_items
1127	 * @param int        $tpl_items[<itemid>]['type']
1128	 * @param string     $tpl_items[<itemid>]['master_itemid']
1129	 * @param array      $new_items
1130	 * @param string     $new_items[<itemid>]['hostid']
1131	 * @param int        $new_items[<itemid>]['type']
1132	 * @param string     $new_items[<itemid>]['templateid']
1133	 * @param array|null $hostids
1134	 *
1135	 * @return array an array of synchronized inherited items.
1136	 */
1137	private function prepareDependentItems(array $tpl_items, array $new_items, array $hostids = null) {
1138		$tpl_master_itemids = [];
1139
1140		foreach ($tpl_items as $tpl_item) {
1141			if ($tpl_item['type'] == ITEM_TYPE_DEPENDENT) {
1142				$tpl_master_itemids[$tpl_item['master_itemid']] = true;
1143			}
1144		}
1145
1146		if ($tpl_master_itemids) {
1147			$sql = 'SELECT i.itemid,i.hostid,i.templateid'.
1148				' FROM items i'.
1149				' WHERE '.dbConditionId('i.templateid', array_keys($tpl_master_itemids));
1150			if ($hostids !== null) {
1151				$sql .= ' AND '.dbConditionId('i.hostid', $hostids);
1152			}
1153			$db_items = DBselect($sql);
1154
1155			$master_links = [];
1156
1157			while ($db_item = DBfetch($db_items)) {
1158				$master_links[$db_item['templateid']][$db_item['hostid']] = $db_item['itemid'];
1159			}
1160
1161			foreach ($new_items as &$new_item) {
1162				if ($new_item['type'] == ITEM_TYPE_DEPENDENT) {
1163					$tpl_item = $tpl_items[$new_item['templateid']];
1164
1165					if (array_key_exists('master_itemid', $tpl_item)) {
1166						$new_item['master_itemid'] = $master_links[$tpl_item['master_itemid']][$new_item['hostid']];
1167					}
1168				}
1169			}
1170			unset($new_item);
1171		}
1172
1173		return $new_items;
1174	}
1175
1176	/**
1177	 * Validate item pre-processing.
1178	 *
1179	 * @param array  $item                                             An array of single item data.
1180	 * @param array  $item['preprocessing']                            An array of item pre-processing data.
1181	 * @param string $item['preprocessing'][]['type']                  The preprocessing option type. Possible values:
1182	 *                                                                  1 - ZBX_PREPROC_MULTIPLIER;
1183	 *                                                                  2 - ZBX_PREPROC_RTRIM;
1184	 *                                                                  3 - ZBX_PREPROC_LTRIM;
1185	 *                                                                  4 - ZBX_PREPROC_TRIM;
1186	 *                                                                  5 - ZBX_PREPROC_REGSUB;
1187	 *                                                                  6 - ZBX_PREPROC_BOOL2DEC;
1188	 *                                                                  7 - ZBX_PREPROC_OCT2DEC;
1189	 *                                                                  8 - ZBX_PREPROC_HEX2DEC;
1190	 *                                                                  9 - ZBX_PREPROC_DELTA_VALUE;
1191	 *                                                                  10 - ZBX_PREPROC_DELTA_SPEED;
1192	 *                                                                  11 - ZBX_PREPROC_XPATH;
1193	 *                                                                  12 - ZBX_PREPROC_JSONPATH;
1194	 *                                                                  13 - ZBX_PREPROC_VALIDATE_RANGE;
1195	 *                                                                  14 - ZBX_PREPROC_VALIDATE_REGEX;
1196	 *                                                                  15 - ZBX_PREPROC_VALIDATE_NOT_REGEX;
1197	 *                                                                  16 - ZBX_PREPROC_ERROR_FIELD_JSON;
1198	 *                                                                  17 - ZBX_PREPROC_ERROR_FIELD_XML;
1199	 *                                                                  18 - ZBX_PREPROC_ERROR_FIELD_REGEX;
1200	 *                                                                  19 - ZBX_PREPROC_THROTTLE_VALUE;
1201	 *                                                                  20 - ZBX_PREPROC_THROTTLE_TIMED_VALUE;
1202	 *                                                                  21 - ZBX_PREPROC_SCRIPT;
1203	 *                                                                  22 - ZBX_PREPROC_PROMETHEUS_PATTERN;
1204	 *                                                                  23 - ZBX_PREPROC_PROMETHEUS_TO_JSON;
1205	 *                                                                  24 - ZBX_PREPROC_CSV_TO_JSON;
1206	 *                                                                  25 - ZBX_PREPROC_STR_REPLACE;
1207	 *                                                                  26 - ZBX_PREPROC_VALIDATE_NOT_SUPPORTED;
1208	 * @param string $item['preprocessing'][]['params']                Additional parameters used by preprocessing
1209	 *                                                                 option. Multiple parameters are separated by LF
1210	 *                                                                 (\n) character.
1211	 * @param string $item['preprocessing'][]['error_handler']         Action type used in case of preprocessing step
1212	 *                                                                 failure. Possible values:
1213	 *                                                                  0 - ZBX_PREPROC_FAIL_DEFAULT;
1214	 *                                                                  1 - ZBX_PREPROC_FAIL_DISCARD_VALUE;
1215	 *                                                                  2 - ZBX_PREPROC_FAIL_SET_VALUE;
1216	 *                                                                  3 - ZBX_PREPROC_FAIL_SET_ERROR.
1217	 * @param string $item['preprocessing'][]['error_handler_params']  Error handler parameters.
1218	 */
1219	protected function validateItemPreprocessing(array $item) {
1220		if (array_key_exists('preprocessing', $item)) {
1221			if (!is_array($item['preprocessing'])) {
1222				self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
1223			}
1224
1225			$type_validator = new CLimitedSetValidator(['values' => static::SUPPORTED_PREPROCESSING_TYPES]);
1226
1227			$error_handler_validator = new CLimitedSetValidator([
1228				'values' => [ZBX_PREPROC_FAIL_DEFAULT, ZBX_PREPROC_FAIL_DISCARD_VALUE, ZBX_PREPROC_FAIL_SET_VALUE,
1229					ZBX_PREPROC_FAIL_SET_ERROR
1230				]
1231			]);
1232
1233			$unsupported_error_handler_validator = new CLimitedSetValidator([
1234				'values' => [ZBX_PREPROC_FAIL_DISCARD_VALUE, ZBX_PREPROC_FAIL_SET_VALUE, ZBX_PREPROC_FAIL_SET_ERROR]
1235			]);
1236
1237			$prometheus_pattern_parser = new CPrometheusPatternParser(['usermacros' => true,
1238				'lldmacros' => ($this instanceof CItemPrototype)
1239			]);
1240			$prometheus_output_parser = new CPrometheusOutputParser(['usermacros' => true,
1241				'lldmacros' => ($this instanceof CItemPrototype)
1242			]);
1243
1244			$required_fields = ['type', 'params', 'error_handler', 'error_handler_params'];
1245			$delta = false;
1246			$throttling = false;
1247			$prometheus = false;
1248
1249			foreach ($item['preprocessing'] as $preprocessing) {
1250				$missing_keys = array_diff($required_fields, array_keys($preprocessing));
1251
1252				if ($missing_keys) {
1253					self::exception(ZBX_API_ERROR_PARAMETERS,
1254						_s('Item pre-processing is missing parameters: %1$s', implode(', ', $missing_keys))
1255					);
1256				}
1257
1258				if (is_array($preprocessing['type'])) {
1259					self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
1260				}
1261				elseif ($preprocessing['type'] === '' || $preprocessing['type'] === null
1262						|| $preprocessing['type'] === false) {
1263					self::exception(ZBX_API_ERROR_PARAMETERS,
1264						_s('Incorrect value for field "%1$s": %2$s.', 'type', _('cannot be empty'))
1265					);
1266				}
1267
1268				if (!$type_validator->validate($preprocessing['type'])) {
1269					self::exception(ZBX_API_ERROR_PARAMETERS,
1270						_s('Incorrect value for field "%1$s": %2$s.', 'type',
1271							_s('unexpected value "%1$s"', $preprocessing['type'])
1272						)
1273					);
1274				}
1275
1276				$preprocessing['params'] = str_replace("\r\n", "\n", $preprocessing['params']);
1277
1278				switch ($preprocessing['type']) {
1279					case ZBX_PREPROC_MULTIPLIER:
1280						// Check if custom multiplier is a valid number.
1281						$params = $preprocessing['params'];
1282
1283						if (is_array($params)) {
1284							self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
1285						}
1286						elseif ($params === '' || $params === null || $params === false) {
1287							self::exception(ZBX_API_ERROR_PARAMETERS,
1288								_s('Incorrect value for field "%1$s": %2$s.', 'params', _('cannot be empty'))
1289							);
1290						}
1291
1292						if (is_numeric($params)) {
1293							break;
1294						}
1295
1296						$types = ['usermacros' => true];
1297
1298						if ($this instanceof CItemPrototype) {
1299							$types['lldmacros'] = true;
1300						}
1301
1302						if (!(new CMacrosResolverGeneral)->getMacroPositions($params, $types)) {
1303							self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
1304								'params', _('a numeric value is expected')
1305							));
1306						}
1307						break;
1308
1309					case ZBX_PREPROC_RTRIM:
1310					case ZBX_PREPROC_LTRIM:
1311					case ZBX_PREPROC_TRIM:
1312					case ZBX_PREPROC_XPATH:
1313					case ZBX_PREPROC_JSONPATH:
1314					case ZBX_PREPROC_VALIDATE_REGEX:
1315					case ZBX_PREPROC_VALIDATE_NOT_REGEX:
1316					case ZBX_PREPROC_ERROR_FIELD_JSON:
1317					case ZBX_PREPROC_ERROR_FIELD_XML:
1318					case ZBX_PREPROC_SCRIPT:
1319						// Check 'params' if not empty.
1320						if (is_array($preprocessing['params'])) {
1321							self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
1322						}
1323						elseif ($preprocessing['params'] === '' || $preprocessing['params'] === null
1324								|| $preprocessing['params'] === false) {
1325							self::exception(ZBX_API_ERROR_PARAMETERS,
1326								_s('Incorrect value for field "%1$s": %2$s.', 'params', _('cannot be empty'))
1327							);
1328						}
1329						break;
1330
1331					case ZBX_PREPROC_REGSUB:
1332					case ZBX_PREPROC_ERROR_FIELD_REGEX:
1333					case ZBX_PREPROC_STR_REPLACE:
1334						// Check if 'params' are not empty and if second parameter contains (after \n) is not empty.
1335						if (is_array($preprocessing['params'])) {
1336							self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
1337						}
1338						elseif ($preprocessing['params'] === '' || $preprocessing['params'] === null
1339								|| $preprocessing['params'] === false) {
1340							self::exception(ZBX_API_ERROR_PARAMETERS,
1341								_s('Incorrect value for field "%1$s": %2$s.', 'params', _('cannot be empty'))
1342							);
1343						}
1344
1345						$params = explode("\n", $preprocessing['params']);
1346
1347						if ($params[0] === '') {
1348							self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
1349								'params', _('first parameter is expected')
1350							));
1351						}
1352
1353						if (($preprocessing['type'] == ZBX_PREPROC_REGSUB
1354								|| $preprocessing['type'] == ZBX_PREPROC_ERROR_FIELD_REGEX)
1355								&& (!array_key_exists(1, $params) || $params[1] === '')) {
1356							self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
1357								'params', _('second parameter is expected')
1358							));
1359						}
1360						break;
1361
1362					case ZBX_PREPROC_VALIDATE_RANGE:
1363						if (is_array($preprocessing['params'])) {
1364							self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
1365						}
1366						elseif (trim($preprocessing['params']) === '' || $preprocessing['params'] === null
1367								|| $preprocessing['params'] === false) {
1368							self::exception(ZBX_API_ERROR_PARAMETERS,
1369								_s('Incorrect value for field "%1$s": %2$s.', 'params', _('cannot be empty'))
1370							);
1371						}
1372
1373						$params = explode("\n", $preprocessing['params']);
1374
1375						if ($params[0] !== '' && !is_numeric($params[0])
1376								&& (new CUserMacroParser())->parse($params[0]) != CParser::PARSE_SUCCESS
1377								&& (!($this instanceof CItemPrototype)
1378									|| ((new CLLDMacroFunctionParser())->parse($params[0]) != CParser::PARSE_SUCCESS
1379										&& (new CLLDMacroParser())->parse($params[0]) != CParser::PARSE_SUCCESS))) {
1380							self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
1381								'params', _('a numeric value is expected')
1382							));
1383						}
1384
1385						if ($params[1] !== '' && !is_numeric($params[1])
1386								&& (new CUserMacroParser())->parse($params[1]) != CParser::PARSE_SUCCESS
1387								&& (!($this instanceof CItemPrototype)
1388									|| ((new CLLDMacroFunctionParser())->parse($params[1]) != CParser::PARSE_SUCCESS
1389										&& (new CLLDMacroParser())->parse($params[1]) != CParser::PARSE_SUCCESS))) {
1390							self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
1391								'params', _('a numeric value is expected')
1392							));
1393						}
1394
1395						if (is_numeric($params[0]) && is_numeric($params[1]) && $params[0] > $params[1]) {
1396							self::exception(ZBX_API_ERROR_PARAMETERS, _s(
1397								'Incorrect value for field "%1$s": %2$s.',
1398								'params',
1399								_s('"%1$s" value must be less than or equal to "%2$s" value', _('min'), _('max'))
1400							));
1401						}
1402						break;
1403
1404					case ZBX_PREPROC_BOOL2DEC:
1405					case ZBX_PREPROC_OCT2DEC:
1406					case ZBX_PREPROC_HEX2DEC:
1407					case ZBX_PREPROC_THROTTLE_VALUE:
1408						// Check if 'params' is empty, because it must be empty.
1409						if (is_array($preprocessing['params'])) {
1410							self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
1411						}
1412						elseif ($preprocessing['params'] !== '' && $preprocessing['params'] !== null
1413								&& $preprocessing['params'] !== false) {
1414							self::exception(ZBX_API_ERROR_PARAMETERS,
1415								_s('Incorrect value for field "%1$s": %2$s.', 'params', _('should be empty'))
1416							);
1417						}
1418
1419						if ($preprocessing['type'] == ZBX_PREPROC_THROTTLE_VALUE) {
1420							if ($throttling) {
1421								self::exception(ZBX_API_ERROR_PARAMETERS, _('Only one throttling step is allowed.'));
1422							}
1423							else {
1424								$throttling = true;
1425							}
1426						}
1427						break;
1428
1429					case ZBX_PREPROC_DELTA_VALUE:
1430					case ZBX_PREPROC_DELTA_SPEED:
1431					case ZBX_PREPROC_XML_TO_JSON:
1432						// Check if 'params' is empty, because it must be empty.
1433						if (is_array($preprocessing['params'])) {
1434							self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
1435						}
1436						elseif ($preprocessing['params'] !== '' && $preprocessing['params'] !== null
1437								&& $preprocessing['params'] !== false) {
1438							self::exception(ZBX_API_ERROR_PARAMETERS,
1439								_s('Incorrect value for field "%1$s": %2$s.', 'params', _('should be empty'))
1440							);
1441						}
1442
1443						if ($preprocessing['type'] == ZBX_PREPROC_DELTA_VALUE
1444								|| $preprocessing['type'] == ZBX_PREPROC_DELTA_SPEED) {
1445							// Check if one of the deltas (Delta per second or Delta value) already exists.
1446							if ($delta) {
1447								self::exception(ZBX_API_ERROR_PARAMETERS, _('Only one change step is allowed.'));
1448							}
1449							else {
1450								$delta = true;
1451							}
1452						}
1453						break;
1454
1455					case ZBX_PREPROC_THROTTLE_TIMED_VALUE:
1456						$api_input_rules = [
1457							'type' => API_TIME_UNIT,
1458							'flags' => ($this instanceof CItem)
1459								? API_NOT_EMPTY | API_ALLOW_USER_MACRO
1460								: API_NOT_EMPTY | API_ALLOW_USER_MACRO | API_ALLOW_LLD_MACRO,
1461							'in' => '1:'.ZBX_MAX_TIMESHIFT
1462						];
1463
1464						if (!CApiInputValidator::validate($api_input_rules, $preprocessing['params'], 'params',
1465								$error)) {
1466							self::exception(ZBX_API_ERROR_PARAMETERS, $error);
1467						}
1468
1469						if ($throttling) {
1470							self::exception(ZBX_API_ERROR_PARAMETERS, _('Only one throttling step is allowed.'));
1471						}
1472						else {
1473							$throttling = true;
1474						}
1475						break;
1476
1477					case ZBX_PREPROC_PROMETHEUS_PATTERN:
1478					case ZBX_PREPROC_PROMETHEUS_TO_JSON:
1479						if ($prometheus) {
1480							self::exception(ZBX_API_ERROR_PARAMETERS, _('Only one Prometheus step is allowed.'));
1481						}
1482
1483						$prometheus = true;
1484
1485						if (is_array($preprocessing['params'])) {
1486							self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
1487						}
1488
1489						if ($preprocessing['type'] == ZBX_PREPROC_PROMETHEUS_PATTERN) {
1490							if ($preprocessing['params'] === '' || $preprocessing['params'] === null
1491									|| $preprocessing['params'] === false) {
1492								self::exception(ZBX_API_ERROR_PARAMETERS,
1493									_s('Incorrect value for field "%1$s": %2$s.', 'params', _('cannot be empty'))
1494								);
1495							}
1496
1497							$params = explode("\n", $preprocessing['params']);
1498
1499							if ($params[0] === '') {
1500								self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
1501									'params', _('first parameter is expected')
1502								));
1503							}
1504
1505							if ($prometheus_pattern_parser->parse($params[0]) != CParser::PARSE_SUCCESS) {
1506								self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
1507									'params', _('invalid Prometheus pattern')
1508								));
1509							}
1510
1511							if ($params[1] !== ''
1512									&& $prometheus_output_parser->parse($params[1]) != CParser::PARSE_SUCCESS) {
1513								self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
1514									'params', _('invalid Prometheus output')
1515								));
1516							}
1517						}
1518						// Prometheus to JSON can be empty and has only one parameter.
1519						elseif ($preprocessing['params'] !== '') {
1520							if ($prometheus_pattern_parser->parse($preprocessing['params']) != CParser::PARSE_SUCCESS) {
1521								self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
1522									'params', _('invalid Prometheus pattern')
1523								));
1524							}
1525						}
1526						break;
1527
1528					case ZBX_PREPROC_CSV_TO_JSON:
1529						if (is_array($preprocessing['params'])) {
1530							self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
1531						}
1532						elseif ($preprocessing['params'] === '' || $preprocessing['params'] === null
1533								|| $preprocessing['params'] === false) {
1534							self::exception(ZBX_API_ERROR_PARAMETERS,
1535								_s('Incorrect value for field "%1$s": %2$s.', 'params', _('cannot be empty'))
1536							);
1537						}
1538
1539						$params = explode("\n", $preprocessing['params']);
1540
1541						$params_cnt = count($params);
1542						if ($params_cnt > 3) {
1543							self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
1544						}
1545						elseif ($params_cnt == 1) {
1546							self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
1547								'params', _('second parameter is expected')
1548							));
1549						}
1550						elseif ($params_cnt == 2) {
1551							self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
1552								'params', _('third parameter is expected')
1553							));
1554						}
1555						else {
1556							// Correct amount of parameters, but check if they are valid.
1557
1558							if (mb_strlen($params[0]) > 1) {
1559								self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
1560									'params', _('value of first parameter is too long')
1561								));
1562							}
1563
1564							if (mb_strlen($params[1]) > 1) {
1565								self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
1566									'params', _('value of second parameter is too long')
1567								));
1568							}
1569
1570							$with_header_row_validator = new CLimitedSetValidator([
1571								'values' => [ZBX_PREPROC_CSV_NO_HEADER, ZBX_PREPROC_CSV_HEADER]
1572							]);
1573
1574							if (!$with_header_row_validator->validate($params[2])) {
1575								self::exception(ZBX_API_ERROR_PARAMETERS,
1576									_s('Incorrect value for field "%1$s": %2$s.', 'params',
1577										_s('value of third parameter must be one of %1$s',
1578											implode(', ', [ZBX_PREPROC_CSV_NO_HEADER, ZBX_PREPROC_CSV_HEADER])
1579										)
1580									)
1581								);
1582							}
1583						}
1584						break;
1585
1586					case ZBX_PREPROC_VALIDATE_NOT_SUPPORTED:
1587						// Check if 'params' is empty, because it must be empty.
1588						if (is_array($preprocessing['params'])) {
1589							self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
1590						}
1591						elseif ($preprocessing['params'] !== '' && $preprocessing['params'] !== null
1592								&& $preprocessing['params'] !== false) {
1593							self::exception(ZBX_API_ERROR_PARAMETERS,
1594								_s('Incorrect value for field "%1$s": %2$s.', 'params', _('should be empty'))
1595							);
1596						}
1597
1598						$preprocessing_types = array_column($item['preprocessing'], 'type');
1599
1600						if (count(array_keys($preprocessing_types, ZBX_PREPROC_VALIDATE_NOT_SUPPORTED)) > 1) {
1601							self::exception(ZBX_API_ERROR_PARAMETERS,
1602								_('Only one not supported value check is allowed.')
1603							);
1604						}
1605						break;
1606				}
1607
1608				switch ($preprocessing['type']) {
1609					case ZBX_PREPROC_RTRIM:
1610					case ZBX_PREPROC_LTRIM:
1611					case ZBX_PREPROC_TRIM:
1612					case ZBX_PREPROC_THROTTLE_VALUE:
1613					case ZBX_PREPROC_THROTTLE_TIMED_VALUE:
1614					case ZBX_PREPROC_SCRIPT:
1615					case ZBX_PREPROC_STR_REPLACE:
1616						if (is_array($preprocessing['error_handler'])) {
1617							self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
1618						}
1619						elseif ($preprocessing['error_handler'] != ZBX_PREPROC_FAIL_DEFAULT) {
1620							self::exception(ZBX_API_ERROR_PARAMETERS,
1621								_s('Incorrect value for field "%1$s": %2$s.', 'error_handler',
1622									_s('unexpected value "%1$s"', $preprocessing['error_handler'])
1623								)
1624							);
1625						}
1626
1627						if (is_array($preprocessing['error_handler_params'])) {
1628							self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
1629						}
1630						elseif ($preprocessing['error_handler_params'] !== ''
1631								&& $preprocessing['error_handler_params'] !== null
1632								&& $preprocessing['error_handler_params'] !== false) {
1633							self::exception(ZBX_API_ERROR_PARAMETERS,
1634								_s('Incorrect value for field "%1$s": %2$s.', 'error_handler_params',
1635									_('should be empty')
1636								)
1637							);
1638						}
1639						break;
1640
1641					case ZBX_PREPROC_VALIDATE_NOT_SUPPORTED:
1642						if (is_array($preprocessing['error_handler'])) {
1643							self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
1644						}
1645						elseif (!$unsupported_error_handler_validator->validate($preprocessing['error_handler'])) {
1646							self::exception(ZBX_API_ERROR_PARAMETERS,
1647								_s('Incorrect value for field "%1$s": %2$s.', 'error_handler',
1648									_s('unexpected value "%1$s"', $preprocessing['error_handler'])
1649								)
1650							);
1651						}
1652
1653						if (is_array($preprocessing['error_handler_params'])) {
1654							self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
1655						}
1656						elseif ($preprocessing['error_handler'] == ZBX_PREPROC_FAIL_DISCARD_VALUE
1657								&& $preprocessing['error_handler_params'] !== ''
1658								&& $preprocessing['error_handler_params'] !== null
1659								&& $preprocessing['error_handler_params'] !== false) {
1660							self::exception(ZBX_API_ERROR_PARAMETERS,
1661								_s('Incorrect value for field "%1$s": %2$s.', 'error_handler_params',
1662									_('should be empty')
1663								)
1664							);
1665						}
1666						elseif ($preprocessing['error_handler'] == ZBX_PREPROC_FAIL_SET_ERROR
1667								&& ($preprocessing['error_handler_params'] === ''
1668									|| $preprocessing['error_handler_params'] === null
1669									|| $preprocessing['error_handler_params'] === false)) {
1670							self::exception(ZBX_API_ERROR_PARAMETERS,
1671								_s('Incorrect value for field "%1$s": %2$s.', 'error_handler_params',
1672									_('cannot be empty')
1673								)
1674							);
1675						}
1676						break;
1677
1678					default:
1679						if (is_array($preprocessing['error_handler'])) {
1680							self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
1681						}
1682						elseif (!$error_handler_validator->validate($preprocessing['error_handler'])) {
1683							self::exception(ZBX_API_ERROR_PARAMETERS,
1684								_s('Incorrect value for field "%1$s": %2$s.', 'error_handler',
1685									_s('unexpected value "%1$s"', $preprocessing['error_handler'])
1686								)
1687							);
1688						}
1689
1690						if (is_array($preprocessing['error_handler_params'])) {
1691							self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
1692						}
1693						elseif (($preprocessing['error_handler'] == ZBX_PREPROC_FAIL_DEFAULT
1694									|| $preprocessing['error_handler'] == ZBX_PREPROC_FAIL_DISCARD_VALUE)
1695								&& $preprocessing['error_handler_params'] !== ''
1696								&& $preprocessing['error_handler_params'] !== null
1697								&& $preprocessing['error_handler_params'] !== false) {
1698							self::exception(ZBX_API_ERROR_PARAMETERS,
1699								_s('Incorrect value for field "%1$s": %2$s.', 'error_handler_params',
1700									_('should be empty')
1701								)
1702							);
1703						}
1704						elseif ($preprocessing['error_handler'] == ZBX_PREPROC_FAIL_SET_ERROR
1705								&& ($preprocessing['error_handler_params'] === ''
1706									|| $preprocessing['error_handler_params'] === null
1707									|| $preprocessing['error_handler_params'] === false)) {
1708							self::exception(ZBX_API_ERROR_PARAMETERS,
1709								_s('Incorrect value for field "%1$s": %2$s.', 'error_handler_params',
1710									_('cannot be empty')
1711								)
1712							);
1713						}
1714				}
1715			}
1716		}
1717	}
1718
1719	/**
1720	 * Method validates preprocessing steps independently from other item properties.
1721	 *
1722	 * @param array  $preprocessing_steps    An array of item pre-processing step details.
1723	 *                                       See self::validateItemPreprocessing for details.
1724	 *
1725	 * @return bool|string
1726	 */
1727	public function validateItemPreprocessingSteps(array $preprocessing_steps) {
1728		try {
1729			$this->validateItemPreprocessing(['preprocessing' => $preprocessing_steps]);
1730
1731			return true;
1732		}
1733		catch (APIException $error) {
1734			return $error->getMessage();
1735		}
1736	}
1737
1738	/**
1739	 * Insert item pre-processing data into DB.
1740	 *
1741	 * @param array  $items                     An array of items.
1742	 * @param string $items[]['itemid']
1743	 * @param array  $items[]['preprocessing']  An array of item pre-processing data.
1744	 */
1745	protected function createItemPreprocessing(array $items) {
1746		$item_preproc = [];
1747
1748		foreach ($items as $item) {
1749			if (array_key_exists('preprocessing', $item)) {
1750				$step = 1;
1751
1752				foreach ($item['preprocessing'] as $preprocessing) {
1753					$item_preproc[] = [
1754						'itemid' => $item['itemid'],
1755						'step' => ($preprocessing['type'] == ZBX_PREPROC_VALIDATE_NOT_SUPPORTED) ? 0 : $step++,
1756						'type' => $preprocessing['type'],
1757						'params' => $preprocessing['params'],
1758						'error_handler' => $preprocessing['error_handler'],
1759						'error_handler_params' => $preprocessing['error_handler_params']
1760					];
1761				}
1762			}
1763		}
1764
1765		if ($item_preproc) {
1766			DB::insertBatch('item_preproc', $item_preproc);
1767		}
1768	}
1769
1770	/**
1771	 * Update item pre-processing data in DB. Delete old records and create new ones.
1772	 *
1773	 * @param array  $items
1774	 * @param string $items[]['itemid']
1775	 * @param array  $items[]['preprocessing']
1776	 * @param int    $items[]['preprocessing'][]['type']
1777	 * @param string $items[]['preprocessing'][]['params']
1778	 * @param int    $items[]['preprocessing'][]['error_handler']
1779	 * @param string $items[]['preprocessing'][]['error_handler_params']
1780	 */
1781	protected function updateItemPreprocessing(array $items) {
1782		$item_preprocs = [];
1783
1784		foreach ($items as $item) {
1785			if (array_key_exists('preprocessing', $item)) {
1786				$item_preprocs[$item['itemid']] = [];
1787				$step = 1;
1788
1789				foreach ($item['preprocessing'] as $item_preproc) {
1790					$curr_step = ($item_preproc['type'] == ZBX_PREPROC_VALIDATE_NOT_SUPPORTED) ? 0 : $step++;
1791					$item_preprocs[$item['itemid']][$curr_step] = [
1792						'type' => $item_preproc['type'],
1793						'params' => $item_preproc['params'],
1794						'error_handler' => $item_preproc['error_handler'],
1795						'error_handler_params' => $item_preproc['error_handler_params']
1796					];
1797				}
1798			}
1799		}
1800
1801		if (!$item_preprocs) {
1802			return;
1803		}
1804
1805		$ins_item_preprocs = [];
1806		$upd_item_preprocs = [];
1807		$del_item_preprocids = [];
1808
1809		$options = [
1810			'output' => ['item_preprocid', 'itemid', 'step', 'type', 'params', 'error_handler', 'error_handler_params'],
1811			'filter' => ['itemid' => array_keys($item_preprocs)]
1812		];
1813		$db_item_preprocs = DBselect(DB::makeSql('item_preproc', $options));
1814
1815		while ($db_item_preproc = DBfetch($db_item_preprocs)) {
1816			if (array_key_exists($db_item_preproc['step'], $item_preprocs[$db_item_preproc['itemid']])) {
1817				$item_preproc = $item_preprocs[$db_item_preproc['itemid']][$db_item_preproc['step']];
1818				$upd_item_preproc = [];
1819
1820				if ($item_preproc['type'] != $db_item_preproc['type']) {
1821					$upd_item_preproc['type'] = $item_preproc['type'];
1822				}
1823				if ($item_preproc['params'] !== $db_item_preproc['params']) {
1824					$upd_item_preproc['params'] = $item_preproc['params'];
1825				}
1826				if ($item_preproc['error_handler'] != $db_item_preproc['error_handler']) {
1827					$upd_item_preproc['error_handler'] = $item_preproc['error_handler'];
1828				}
1829				if ($item_preproc['error_handler_params'] !== $db_item_preproc['error_handler_params']) {
1830					$upd_item_preproc['error_handler_params'] = $item_preproc['error_handler_params'];
1831				}
1832
1833				if ($upd_item_preproc) {
1834					$upd_item_preprocs[] = [
1835						'values' => $upd_item_preproc,
1836						'where' => ['item_preprocid' => $db_item_preproc['item_preprocid']]
1837					];
1838				}
1839				unset($item_preprocs[$db_item_preproc['itemid']][$db_item_preproc['step']]);
1840			}
1841			else {
1842				$del_item_preprocids[] = $db_item_preproc['item_preprocid'];
1843			}
1844		}
1845
1846		foreach ($item_preprocs as $itemid => $preprocs) {
1847			foreach ($preprocs as $step => $preproc) {
1848				$ins_item_preprocs[] = [
1849					'itemid' => $itemid,
1850					'step' => $step
1851				] + $preproc;
1852			}
1853		}
1854
1855		if ($del_item_preprocids) {
1856			DB::delete('item_preproc', ['item_preprocid' => $del_item_preprocids]);
1857		}
1858
1859		if ($upd_item_preprocs) {
1860			DB::update('item_preproc', $upd_item_preprocs);
1861		}
1862
1863		if ($ins_item_preprocs) {
1864			DB::insertBatch('item_preproc', $ins_item_preprocs);
1865		}
1866	}
1867
1868	/**
1869	 * Create item parameters.
1870	 *
1871	 * @param array $items                             Array of items.
1872	 * @param array $items[]['parameters']             Item parameters.
1873	 * @param array $items[]['parameters'][]['name']   Parameter name.
1874	 * @param array $items[]['parameters'][]['value']  Parameter value.
1875	 * @param array $itemids                           Array of item IDs that were created before.
1876	 */
1877	protected function createItemParameters(array $items, array $itemids): void {
1878		$item_parameters = [];
1879
1880		foreach ($items as $key => $item) {
1881			$items[$key]['itemid'] = $itemids[$key];
1882
1883			if (!array_key_exists('parameters', $item) || !$item['parameters']) {
1884				continue;
1885			}
1886
1887			foreach ($item['parameters'] as $parameter) {
1888				$item_parameters[] = [
1889					'itemid' => $items[$key]['itemid'],
1890					'name' => $parameter['name'],
1891					'value' => $parameter['value']
1892				];
1893			}
1894		}
1895
1896		if ($item_parameters) {
1897			DB::insertBatch('item_parameter', $item_parameters);
1898		}
1899	}
1900
1901	/**
1902	 * Update item parameters.
1903	 *
1904	 * @param array      $items                             Array of items.
1905	 * @param int|string $items[]['itemid']                 Item ID.
1906	 * @param int|string $items[]['type']                   Item type.
1907	 * @param array      $items[]['parameters']             Item parameters.
1908	 * @param array      $items[]['parameters'][]['name']   Parameter name.
1909	 * @param array      $items[]['parameters'][]['value']  Parameter value.
1910	 */
1911	protected function updateItemParameters(array $items): void {
1912		$db_item_parameters_by_itemid = [];
1913
1914		foreach ($items as $item) {
1915			if ($item['type'] != ITEM_TYPE_SCRIPT || array_key_exists('parameters', $item)) {
1916				$db_item_parameters_by_itemid[$item['itemid']] = [];
1917			}
1918		}
1919
1920		if (!$db_item_parameters_by_itemid) {
1921			return;
1922		}
1923
1924		$options = [
1925			'output' => ['item_parameterid', 'itemid', 'name', 'value'],
1926			'filter' => ['itemid' => array_keys($db_item_parameters_by_itemid)]
1927		];
1928		$result = DBselect(DB::makeSql('item_parameter', $options));
1929
1930		while ($row = DBfetch($result)) {
1931			$db_item_parameters_by_itemid[$row['itemid']][$row['name']] = [
1932				'item_parameterid' => $row['item_parameterid'],
1933				'value' => $row['value']
1934			];
1935		}
1936
1937		$ins_item_parameters = [];
1938		$upd_item_parameters = [];
1939		$del_item_parameterids = [];
1940
1941		foreach ($db_item_parameters_by_itemid as $itemid => $db_item_parameters) {
1942			$item = $items[$itemid];
1943
1944			if ($item['type'] == ITEM_TYPE_SCRIPT && array_key_exists('parameters', $item)) {
1945				foreach ($item['parameters'] as $parameter) {
1946					if (array_key_exists($parameter['name'], $db_item_parameters)) {
1947						if ($db_item_parameters[$parameter['name']]['value'] !== $parameter['value']) {
1948							$upd_item_parameters[] = [
1949								'values' => ['value' => $parameter['value']],
1950								'where' => [
1951									'item_parameterid' => $db_item_parameters[$parameter['name']]['item_parameterid']
1952								]
1953							];
1954						}
1955						unset($db_item_parameters[$parameter['name']]);
1956					}
1957					else {
1958						$ins_item_parameters[] = [
1959							'itemid' => $itemid,
1960							'name' => $parameter['name'],
1961							'value' => $parameter['value']
1962						];
1963					}
1964				}
1965			}
1966
1967			$del_item_parameterids = array_merge($del_item_parameterids,
1968				array_column($db_item_parameters, 'item_parameterid')
1969			);
1970		}
1971
1972		if ($del_item_parameterids) {
1973			DB::delete('item_parameter', ['item_parameterid' => $del_item_parameterids]);
1974		}
1975
1976		if ($upd_item_parameters) {
1977			DB::update('item_parameter', $upd_item_parameters);
1978		}
1979
1980		if ($ins_item_parameters) {
1981			DB::insertBatch('item_parameter', $ins_item_parameters);
1982		}
1983	}
1984
1985	/**
1986	 * Check if any item from list already exists.
1987	 * If items have item ids it will check for existing item with different itemid.
1988	 *
1989	 * @throw APIException
1990	 *
1991	 * @param array $items
1992	 */
1993	protected function checkExistingItems(array $items) {
1994		$itemKeysByHostId = [];
1995		$itemIds = [];
1996		foreach ($items as $item) {
1997			if (!isset($itemKeysByHostId[$item['hostid']])) {
1998				$itemKeysByHostId[$item['hostid']] = [];
1999			}
2000			$itemKeysByHostId[$item['hostid']][] = $item['key_'];
2001
2002			if (isset($item['itemid'])) {
2003				$itemIds[] = $item['itemid'];
2004			}
2005		}
2006
2007		$sqlWhere = [];
2008		foreach ($itemKeysByHostId as $hostId => $keys) {
2009			$sqlWhere[] = '(i.hostid='.zbx_dbstr($hostId).' AND '.dbConditionString('i.key_', $keys).')';
2010		}
2011
2012		if ($sqlWhere) {
2013			$sql = 'SELECT i.key_,h.host'.
2014					' FROM items i,hosts h'.
2015					' WHERE i.hostid=h.hostid AND ('.implode(' OR ', $sqlWhere).')';
2016
2017			// if we update existing items we need to exclude them from result.
2018			if ($itemIds) {
2019				$sql .= ' AND '.dbConditionInt('i.itemid', $itemIds, true);
2020			}
2021			$dbItems = DBselect($sql, 1);
2022			while ($dbItem = DBfetch($dbItems)) {
2023				self::exception(ZBX_API_ERROR_PARAMETERS,
2024					_s('Item with key "%1$s" already exists on "%2$s".', $dbItem['key_'], $dbItem['host']));
2025			}
2026		}
2027	}
2028
2029	protected function addRelatedObjects(array $options, array $result) {
2030		$result = parent::addRelatedObjects($options, $result);
2031
2032		// adding hosts
2033		if ($options['selectHosts'] !== null && $options['selectHosts'] != API_OUTPUT_COUNT) {
2034			$relationMap = $this->createRelationMap($result, 'itemid', 'hostid');
2035			$hosts = API::Host()->get([
2036				'hostids' => $relationMap->getRelatedIds(),
2037				'templated_hosts' => true,
2038				'output' => $options['selectHosts'],
2039				'nopermissions' => true,
2040				'preservekeys' => true
2041			]);
2042			$result = $relationMap->mapMany($result, $hosts, 'hosts');
2043		}
2044
2045		// adding preprocessing
2046		if ($options['selectPreprocessing'] !== null && $options['selectPreprocessing'] != API_OUTPUT_COUNT) {
2047			$db_item_preproc = API::getApiService()->select('item_preproc', [
2048				'output' => $this->outputExtend($options['selectPreprocessing'], ['itemid', 'step']),
2049				'filter' => ['itemid' => array_keys($result)]
2050			]);
2051
2052			CArrayHelper::sort($db_item_preproc, ['step']);
2053
2054			foreach ($result as &$item) {
2055				$item['preprocessing'] = [];
2056			}
2057			unset($item);
2058
2059			foreach ($db_item_preproc as $step) {
2060				$itemid = $step['itemid'];
2061				unset($step['item_preprocid'], $step['itemid'], $step['step']);
2062
2063				if (array_key_exists($itemid, $result)) {
2064					$result[$itemid]['preprocessing'][] = $step;
2065				}
2066			}
2067		}
2068
2069		// Add value mapping.
2070		if (($this instanceof CItemPrototype || $this instanceof CItem) && $options['selectValueMap'] !== null) {
2071			if ($options['selectValueMap'] === API_OUTPUT_EXTEND) {
2072				$options['selectValueMap'] = ['valuemapid', 'name', 'mappings'];
2073			}
2074
2075			foreach ($result as &$item) {
2076				$item['valuemap'] = [];
2077			}
2078			unset($item);
2079
2080			$valuemaps = DB::select('valuemap', [
2081				'output' => array_diff($this->outputExtend($options['selectValueMap'], ['valuemapid', 'hostid']),
2082					['mappings']
2083				),
2084				'filter' => ['valuemapid' => array_keys(array_flip(array_column($result, 'valuemapid')))],
2085				'preservekeys' => true
2086			]);
2087
2088			if ($this->outputIsRequested('mappings', $options['selectValueMap']) && $valuemaps) {
2089				$params = [
2090					'output' => ['valuemapid', 'type', 'value', 'newvalue'],
2091					'filter' => ['valuemapid' => array_keys($valuemaps)],
2092					'sortfield' => ['sortorder']
2093				];
2094				$query = DBselect(DB::makeSql('valuemap_mapping', $params));
2095
2096				while ($mapping = DBfetch($query)) {
2097					$valuemaps[$mapping['valuemapid']]['mappings'][] = [
2098						'type' => $mapping['type'],
2099						'value' => $mapping['value'],
2100						'newvalue' => $mapping['newvalue']
2101					];
2102				}
2103			}
2104
2105			foreach ($result as &$item) {
2106				if (array_key_exists('valuemapid', $item) && array_key_exists($item['valuemapid'], $valuemaps)) {
2107					$item['valuemap'] = array_intersect_key($valuemaps[$item['valuemapid']],
2108						array_flip($options['selectValueMap'])
2109					);
2110				}
2111			}
2112			unset($item);
2113		}
2114
2115		if (!$options['countOutput'] && $this->outputIsRequested('parameters', $options['output'])) {
2116			$item_parameters = DBselect(
2117				'SELECT ip.itemid,ip.name,ip.value'.
2118				' FROM item_parameter ip'.
2119				' WHERE '.dbConditionInt('ip.itemid', array_keys($result))
2120			);
2121
2122			foreach ($result as &$item) {
2123				$item['parameters'] = [];
2124			}
2125			unset($item);
2126
2127			while ($row = DBfetch($item_parameters)) {
2128				$result[$row['itemid']]['parameters'][] = [
2129					'name' =>  $row['name'],
2130					'value' =>  $row['value']
2131				];
2132			}
2133		}
2134
2135		return $result;
2136	}
2137
2138	/**
2139	 * Validate items with type ITEM_TYPE_DEPENDENT for create or update operation.
2140	 *
2141	 * @param array  $items
2142	 * @param string $items[]['itemid']         (mandatory for updated items and item prototypes)
2143	 * @param string $items[]['hostid']
2144	 * @param int    $items[]['type']
2145	 * @param string $items[]['master_itemid']  (mandatory for ITEM_TYPE_DEPENDENT)
2146	 * @param int    $items[]['flags']          (mandatory for items)
2147	 *
2148	 * @throws APIException for invalid data.
2149	 */
2150	protected function validateDependentItems(array $items) {
2151		$dep_items = [];
2152		$upd_itemids = [];
2153
2154		foreach ($items as $item) {
2155			if ($item['type'] == ITEM_TYPE_DEPENDENT) {
2156				if ($this instanceof CDiscoveryRule || $this instanceof CItemPrototype
2157						|| $item['flags'] == ZBX_FLAG_DISCOVERY_NORMAL) {
2158					$dep_items[] = $item;
2159				}
2160
2161				if (array_key_exists('itemid', $item)) {
2162					$upd_itemids[] = $item['itemid'];
2163				}
2164			}
2165		}
2166
2167		if (!$dep_items) {
2168			return;
2169		}
2170
2171		if ($this instanceof CItemPrototype && $upd_itemids) {
2172			$db_links = DBselect(
2173				'SELECT id.itemid,id.parent_itemid AS ruleid'.
2174				' FROM item_discovery id'.
2175				' WHERE '.dbConditionId('id.itemid', $upd_itemids)
2176			);
2177
2178			$links = [];
2179
2180			while ($db_link = DBfetch($db_links)) {
2181				$links[$db_link['itemid']] = $db_link['ruleid'];
2182			}
2183
2184			foreach ($dep_items as &$dep_item) {
2185				if (array_key_exists('itemid', $dep_item)) {
2186					$dep_item['ruleid'] = $links[$dep_item['itemid']];
2187				}
2188			}
2189			unset($dep_item);
2190		}
2191
2192		$master_itemids = [];
2193
2194		foreach ($dep_items as $dep_item) {
2195			$master_itemids[$dep_item['master_itemid']] = true;
2196		}
2197
2198		$master_items = [];
2199
2200		// Fill relations array by master items (item prototypes). Discovery rule should not be master item.
2201		do {
2202			if ($this instanceof CItemPrototype) {
2203				$db_master_items = DBselect(
2204					'SELECT i.itemid,i.hostid,i.master_itemid,i.flags,id.parent_itemid AS ruleid'.
2205					' FROM items i'.
2206						' LEFT JOIN item_discovery id'.
2207							' ON i.itemid=id.itemid'.
2208					' WHERE '.dbConditionId('i.itemid', array_keys($master_itemids)).
2209						' AND '.dbConditionInt('i.flags', [ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_PROTOTYPE])
2210				);
2211			}
2212			// CDiscoveryRule, CItem
2213			else {
2214				$db_master_items = DBselect(
2215					'SELECT i.itemid,i.hostid,i.master_itemid'.
2216					' FROM items i'.
2217					' WHERE '.dbConditionId('i.itemid', array_keys($master_itemids)).
2218						' AND '.dbConditionInt('i.flags', [ZBX_FLAG_DISCOVERY_NORMAL])
2219				);
2220			}
2221
2222			while ($db_master_item = DBfetch($db_master_items)) {
2223				$master_items[$db_master_item['itemid']] = $db_master_item;
2224
2225				unset($master_itemids[$db_master_item['itemid']]);
2226			}
2227
2228			if ($master_itemids) {
2229				reset($master_itemids);
2230
2231				self::exception(ZBX_API_ERROR_PERMISSIONS,
2232					_s('Incorrect value for field "%1$s": %2$s.', 'master_itemid',
2233						_s('Item "%1$s" does not exist or you have no access to this item', key($master_itemids))
2234					)
2235				);
2236			}
2237
2238			$master_itemids = [];
2239
2240			foreach ($master_items as $master_item) {
2241				if ($master_item['master_itemid'] != 0
2242						&& !array_key_exists($master_item['master_itemid'], $master_items)) {
2243					$master_itemids[$master_item['master_itemid']] = true;
2244				}
2245			}
2246		} while ($master_itemids);
2247
2248		foreach ($dep_items as $dep_item) {
2249			$master_item = $master_items[$dep_item['master_itemid']];
2250
2251			if ($dep_item['hostid'] != $master_item['hostid']) {
2252				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
2253					'master_itemid', _('hostid of dependent item and master item should match')
2254				));
2255			}
2256
2257			if ($this instanceof CItemPrototype && $master_item['flags'] == ZBX_FLAG_DISCOVERY_PROTOTYPE
2258					&& $dep_item['ruleid'] != $master_item['ruleid']) {
2259				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
2260					'master_itemid', _('ruleid of dependent item and master item should match')
2261				));
2262			}
2263
2264			if (array_key_exists('itemid', $dep_item)) {
2265				$master_itemid = $dep_item['master_itemid'];
2266
2267				while ($master_itemid != 0) {
2268					if ($master_itemid == $dep_item['itemid']) {
2269						self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
2270							'master_itemid', _('circular item dependency is not allowed')
2271						));
2272					}
2273
2274					$master_itemid = $master_items[$master_itemid]['master_itemid'];
2275				}
2276			}
2277		}
2278
2279		// Fill relations array by dependent items (item prototypes).
2280		$root_itemids = [];
2281
2282		foreach ($master_items as $master_item) {
2283			if ($master_item['master_itemid'] == 0) {
2284				$root_itemids[] = $master_item['itemid'];
2285			}
2286		}
2287
2288		$dependent_items = [];
2289
2290		foreach ($dep_items as $dep_item) {
2291			if (array_key_exists('itemid', $dep_item)) {
2292				$dependent_items[$dep_item['master_itemid']][] = $dep_item['itemid'];
2293			}
2294		}
2295
2296		$master_itemids = $root_itemids;
2297
2298		do {
2299			$sql = 'SELECT i.master_itemid,i.itemid'.
2300				' FROM items i'.
2301				' WHERE '.dbConditionId('i.master_itemid', $master_itemids);
2302			if ($upd_itemids) {
2303				$sql .= ' AND '.dbConditionId('i.itemid', $upd_itemids, true); // Exclude updated items.
2304			}
2305
2306			$db_items = DBselect($sql);
2307
2308			while ($db_item = DBfetch($db_items)) {
2309				$dependent_items[$db_item['master_itemid']][] = $db_item['itemid'];
2310			}
2311
2312			$_master_itemids = $master_itemids;
2313			$master_itemids = [];
2314
2315			foreach ($_master_itemids as $master_itemid) {
2316				if (array_key_exists($master_itemid, $dependent_items)) {
2317					$master_itemids = array_merge($master_itemids, $dependent_items[$master_itemid]);
2318				}
2319			}
2320		} while ($master_itemids);
2321
2322		foreach ($dep_items as $dep_item) {
2323			if (!array_key_exists('itemid', $dep_item)) {
2324				$dependent_items[$dep_item['master_itemid']][] = false;
2325			}
2326		}
2327
2328		foreach ($root_itemids as $root_itemid) {
2329			self::checkDependencyDepth($dependent_items, $root_itemid);
2330		}
2331	}
2332
2333	/**
2334	 * Validate depth and amount of elements in the tree of the dependent items.
2335	 *
2336	 * @param array  $dependent_items
2337	 * @param string $dependent_items[<master_itemid>][]  List if the dependent item IDs ("false" for new items)
2338	 *                                                    by master_itemid.
2339	 * @param string $root_itemid                         ID of the item being checked.
2340	 * @param int    $level                               Current dependency level.
2341	 *
2342	 * @throws APIException for invalid data.
2343	 */
2344	private static function checkDependencyDepth(array $dependent_items, $root_itemid, $level = 0) {
2345		$count = 0;
2346
2347		if (array_key_exists($root_itemid, $dependent_items)) {
2348			if (++$level > ZBX_DEPENDENT_ITEM_MAX_LEVELS) {
2349				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
2350					'master_itemid', _('maximum number of dependency levels reached')
2351				));
2352			}
2353
2354			foreach ($dependent_items[$root_itemid] as $master_itemid) {
2355				$count++;
2356
2357				if ($master_itemid !== false) {
2358					$count += self::checkDependencyDepth($dependent_items, $master_itemid, $level);
2359				}
2360			}
2361
2362			if ($count > ZBX_DEPENDENT_ITEM_MAX_COUNT) {
2363				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
2364					'master_itemid', _('maximum dependent items count reached')
2365				));
2366			}
2367		}
2368
2369		return $count;
2370	}
2371
2372	/**
2373	 * Converts headers field text to hash with header name as key.
2374	 *
2375	 * @param string $headers  Headers string, one header per line, line delimiter "\r\n".
2376	 *
2377	 * @return array
2378	 */
2379	protected function headersStringToArray($headers) {
2380		$result = [];
2381
2382		foreach (explode("\r\n", $headers) as $header) {
2383			$header = explode(': ', $header, 2);
2384
2385			if (count($header) == 2) {
2386				$result[$header[0]] = $header[1];
2387			}
2388		}
2389
2390		return $result;
2391	}
2392
2393	/**
2394	 * Converts headers fields hash to string.
2395	 *
2396	 * @param array $headers  Array of headers where key is header name.
2397	 *
2398	 * @return string
2399	 */
2400	protected function headersArrayToString(array $headers) {
2401		$result = [];
2402
2403		foreach ($headers as $k => $v) {
2404			$result[] = $k.': '.$v;
2405		}
2406
2407		return implode("\r\n", $result);
2408	}
2409
2410	/**
2411	 * Validate item with type ITEM_TYPE_HTTPAGENT.
2412	 *
2413	 * @param array $item     Array of item fields.
2414	 * @param array $db_item  Array of item database fields for update action or empty array for create action.
2415	 *
2416	 * @throws APIException for invalid data.
2417	 */
2418	protected function validateHTTPCheck(array $item, array $db_item) {
2419		$rules = [
2420			'timeout' => [
2421				'type' => API_TIME_UNIT, 'flags' => ($this instanceof CItemPrototype)
2422					? API_NOT_EMPTY | API_ALLOW_USER_MACRO | API_ALLOW_LLD_MACRO
2423					: API_NOT_EMPTY | API_ALLOW_USER_MACRO,
2424				'in' => '1:'.SEC_PER_MIN
2425			],
2426			'url' => [
2427				'type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY,
2428				'length' => DB::getFieldLength('items', 'url')
2429			],
2430			'status_codes' => [
2431				'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('items', 'status_codes')
2432			],
2433			'follow_redirects' => [
2434				'type' => API_INT32,
2435				'in' => implode(',', [HTTPTEST_STEP_FOLLOW_REDIRECTS_OFF, HTTPTEST_STEP_FOLLOW_REDIRECTS_ON])
2436			],
2437			'post_type' => [
2438				'type' => API_INT32,
2439				'in' => implode(',', [ZBX_POSTTYPE_RAW, ZBX_POSTTYPE_JSON, ZBX_POSTTYPE_XML])
2440			],
2441			'http_proxy' => [
2442				'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('items', 'http_proxy')
2443			],
2444			'headers' => [
2445				'type' => API_STRINGS_UTF8
2446			],
2447			'retrieve_mode' => [
2448				'type' => API_INT32,
2449				'in' => implode(',', [
2450					HTTPTEST_STEP_RETRIEVE_MODE_CONTENT, HTTPTEST_STEP_RETRIEVE_MODE_HEADERS,
2451					HTTPTEST_STEP_RETRIEVE_MODE_BOTH
2452				])
2453			],
2454			'request_method' => [
2455				'type' => API_INT32,
2456				'in' => implode(',', [
2457					HTTPCHECK_REQUEST_GET, HTTPCHECK_REQUEST_POST, HTTPCHECK_REQUEST_PUT, HTTPCHECK_REQUEST_HEAD
2458				])
2459			],
2460			'output_format' => [
2461				'type' => API_INT32,
2462				'in' => implode(',', [HTTPCHECK_STORE_RAW, HTTPCHECK_STORE_JSON])
2463			],
2464			'allow_traps' => [
2465				'type' => API_INT32,
2466				'in' => implode(',', [HTTPCHECK_ALLOW_TRAPS_OFF, HTTPCHECK_ALLOW_TRAPS_ON])
2467			],
2468			'ssl_cert_file' => [
2469				'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('items', 'ssl_cert_file')
2470			],
2471			'ssl_key_file' => [
2472				'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('items', 'ssl_key_file')
2473			],
2474			'ssl_key_password' => [
2475				'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('items', 'ssl_key_password')
2476			],
2477			'verify_peer' => [
2478				'type' => API_INT32,
2479				'in' => implode(',', [HTTPTEST_VERIFY_PEER_OFF, HTTPTEST_VERIFY_PEER_ON])
2480			],
2481			'verify_host' => [
2482				'type' => API_INT32,
2483				'in' => implode(',', [HTTPTEST_VERIFY_HOST_OFF, HTTPTEST_VERIFY_HOST_ON])
2484			],
2485			'authtype' => [
2486				'type' => API_INT32,
2487				'in' => implode(',', [
2488					HTTPTEST_AUTH_NONE, HTTPTEST_AUTH_BASIC, HTTPTEST_AUTH_NTLM, HTTPTEST_AUTH_KERBEROS,
2489					HTTPTEST_AUTH_DIGEST
2490				])
2491			]
2492		];
2493
2494		$data = $item + $db_item;
2495
2496		if (array_key_exists('authtype', $data)
2497				&& ($data['authtype'] == HTTPTEST_AUTH_BASIC || $data['authtype'] == HTTPTEST_AUTH_NTLM
2498					|| $data['authtype'] == HTTPTEST_AUTH_KERBEROS || $data['authtype'] == HTTPTEST_AUTH_DIGEST)) {
2499			$rules += [
2500				'username' => [ 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('items', 'username')],
2501				'password' => [ 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('items', 'password')]
2502			];
2503		}
2504
2505		// Strict validation for 'retrieve_mode' only for create action.
2506		if (array_key_exists('request_method', $data) && $data['request_method'] == HTTPCHECK_REQUEST_HEAD
2507				&& array_key_exists('retrieve_mode', $item)) {
2508			$rules['retrieve_mode']['in'] = (string) HTTPTEST_STEP_RETRIEVE_MODE_HEADERS;
2509		}
2510
2511		if (array_key_exists('post_type', $data)
2512				&& ($data['post_type'] == ZBX_POSTTYPE_JSON || $data['post_type'] == ZBX_POSTTYPE_XML)) {
2513			$rules['posts'] = [
2514				'type' => API_STRING_UTF8,
2515				'length' => DB::getFieldLength('items', 'posts')
2516			];
2517		}
2518
2519		if (array_key_exists('templateid', $data) && $data['templateid']) {
2520			$rules['interfaceid'] = [
2521				'type' => API_ID, 'flags' => API_REQUIRED | API_NOT_EMPTY
2522			];
2523		}
2524
2525		if (array_key_exists('trapper_hosts', $item) && $item['trapper_hosts'] !== ''
2526				&& (!array_key_exists('allow_traps', $data) || $data['allow_traps'] == HTTPCHECK_ALLOW_TRAPS_OFF)) {
2527			self::exception(ZBX_API_ERROR_PARAMETERS,
2528				_s('Incorrect value for field "%1$s": %2$s.', 'trapper_hosts', _('should be empty'))
2529			);
2530		}
2531
2532		// Keep values only for fields with defined validation rules.
2533		$data = array_intersect_key($data, $rules);
2534
2535		if (!CApiInputValidator::validate(['type' => API_OBJECT, 'fields' => $rules], $data, '', $error)) {
2536			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
2537		}
2538
2539		if (array_key_exists('query_fields', $item)) {
2540			if (!is_array($item['query_fields'])) {
2541				self::exception(ZBX_API_ERROR_PARAMETERS,
2542					_s('Invalid parameter "%1$s": %2$s.', 'query_fields', _('an array is expected'))
2543				);
2544			}
2545
2546			foreach ($item['query_fields'] as $v) {
2547				if (!is_array($v) || count($v) > 1 || key($v) === '') {
2548					self::exception(ZBX_API_ERROR_PARAMETERS,
2549						_s('Invalid parameter "%1$s": %2$s.', 'query_fields', _('nonempty key and value pair expected'))
2550					);
2551				}
2552			}
2553
2554			if (strlen(json_encode($item['query_fields'])) > DB::getFieldLength('items', 'query_fields')) {
2555				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.', 'query_fields',
2556					_('cannot convert to JSON, result value too long')
2557				));
2558			}
2559		}
2560
2561		if (array_key_exists('headers', $item)) {
2562			if (!is_array($item['headers'])) {
2563				self::exception(ZBX_API_ERROR_PARAMETERS,
2564					_s('Invalid parameter "%1$s": %2$s.', 'headers', _('an array is expected'))
2565				);
2566			}
2567
2568			foreach ($item['headers'] as $k => $v) {
2569				if (trim($k) === '' || !is_string($v) || $v === '') {
2570					self::exception(ZBX_API_ERROR_PARAMETERS,
2571						_s('Invalid parameter "%1$s": %2$s.', 'headers', _('nonempty key and value pair expected'))
2572					);
2573				}
2574			}
2575		}
2576
2577		if (array_key_exists('status_codes', $item) && $item['status_codes']) {
2578			$ranges_parser = new CRangesParser([
2579				'usermacros' => true,
2580				'lldmacros' => ($this instanceof CItemPrototype)
2581			]);
2582
2583			if ($ranges_parser->parse($item['status_codes']) != CParser::PARSE_SUCCESS) {
2584				self::exception(ZBX_API_ERROR_PARAMETERS,
2585					_s('Incorrect value "%1$s" for "%2$s" field.', $item['status_codes'], 'status_codes')
2586				);
2587			}
2588		}
2589
2590		if ((array_key_exists('post_type', $item) || array_key_exists('posts', $item))
2591				&& ($data['post_type'] == ZBX_POSTTYPE_JSON || $data['post_type'] == ZBX_POSTTYPE_XML)) {
2592			$posts = array_key_exists('posts', $data) ? $data['posts'] : '';
2593			libxml_use_internal_errors(true);
2594
2595			if ($data['post_type'] == ZBX_POSTTYPE_XML
2596					&& simplexml_load_string($posts, null, LIBXML_IMPORT_FLAGS) === false) {
2597				$errors = libxml_get_errors();
2598				libxml_clear_errors();
2599
2600				if (!$errors) {
2601					self::exception(ZBX_API_ERROR_PARAMETERS,
2602						_s('Invalid parameter "%1$s": %2$s.', 'posts', _('XML is expected'))
2603					);
2604				}
2605				else {
2606					$error = reset($errors);
2607					self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.', 'posts',
2608						_s('%1$s [Line: %2$s | Column: %3$s]', '('.$error->code.') '.trim($error->message),
2609						$error->line, $error->column
2610					)));
2611				}
2612			}
2613
2614			if ($data['post_type'] == ZBX_POSTTYPE_JSON) {
2615				if (trim($posts, " \r\n") === '') {
2616					self::exception(ZBX_API_ERROR_PARAMETERS,
2617						_s('Invalid parameter "%1$s": %2$s.', 'posts', _('JSON is expected'))
2618					);
2619				}
2620
2621				$types = [
2622					'usermacros' => true,
2623					'macros_n' => [
2624						'{HOST.IP}', '{HOST.CONN}', '{HOST.DNS}', '{HOST.HOST}', '{HOST.NAME}', '{ITEM.ID}',
2625						'{ITEM.KEY}'
2626					]
2627				];
2628
2629				if ($this instanceof CItemPrototype) {
2630					$types['lldmacros'] = true;
2631				}
2632
2633				$matches = (new CMacrosResolverGeneral)->getMacroPositions($posts, $types);
2634
2635				$shift = 0;
2636
2637				foreach ($matches as $pos => $substr) {
2638					$posts = substr_replace($posts, '1', $pos + $shift, strlen($substr));
2639					$shift = $shift + 1 - strlen($substr);
2640				}
2641
2642				json_decode($posts);
2643
2644				if (json_last_error()) {
2645					self::exception(ZBX_API_ERROR_PARAMETERS,
2646						_s('Invalid parameter "%1$s": %2$s.', 'posts', _('JSON is expected'))
2647					);
2648				}
2649			}
2650		}
2651	}
2652
2653	/**
2654	 * Remove NCLOB value type fields from resulting query SELECT part if DISTINCT will be used.
2655	 *
2656	 * @param string $table_name     Table name.
2657	 * @param string $table_alias    Table alias value.
2658	 * @param array  $options        Array of query options.
2659	 * @param array  $sql_parts      Array of query parts already initialized from $options.
2660	 *
2661	 * @return array    The resulting SQL parts array.
2662	 */
2663	protected function applyQueryOutputOptions($table_name, $table_alias, array $options, array $sql_parts) {
2664		if (!$options['countOutput'] && self::dbDistinct($sql_parts)) {
2665			$schema = $this->getTableSchema();
2666			$nclob_fields = [];
2667
2668			foreach ($schema['fields'] as $field_name => $field) {
2669				if ($field['type'] == DB::FIELD_TYPE_NCLOB
2670						&& $this->outputIsRequested($field_name, $options['output'])) {
2671					$nclob_fields[] = $field_name;
2672				}
2673			}
2674
2675			if ($nclob_fields) {
2676				$output = ($options['output'] === API_OUTPUT_EXTEND)
2677					? array_keys($schema['fields'])
2678					: $options['output'];
2679
2680				$options['output'] = array_diff($output, $nclob_fields);
2681			}
2682		}
2683
2684		return parent::applyQueryOutputOptions($table_name, $table_alias, $options, $sql_parts);
2685	}
2686
2687	/**
2688	 * Add NCLOB type fields if there was DISTINCT in query.
2689	 *
2690	 * @param array $options    Array of query options.
2691	 * @param array $result     Query results.
2692	 *
2693	 * @return array    The result array with added NCLOB fields.
2694	 */
2695	protected function addNclobFieldValues(array $options, array $result): array {
2696		$schema = $this->getTableSchema();
2697		$nclob_fields = [];
2698
2699		foreach ($schema['fields'] as $field_name => $field) {
2700			if ($field['type'] == DB::FIELD_TYPE_NCLOB && $this->outputIsRequested($field_name, $options['output'])) {
2701				$nclob_fields[] = $field_name;
2702			}
2703		}
2704
2705		if (!$nclob_fields) {
2706			return $result;
2707		}
2708
2709		$pk = $schema['key'];
2710		$options = [
2711			'output' => $nclob_fields,
2712			'filter' => [$pk => array_keys($result)]
2713		];
2714
2715		$db_items = DBselect(DB::makeSql($this->tableName, $options));
2716
2717		while ($db_item = DBfetch($db_items)) {
2718			$result[$db_item[$pk]] += $db_item;
2719		}
2720
2721		return $result;
2722	}
2723
2724	/**
2725	 * Update item tags.
2726	 *
2727	 * @param array  $items
2728	 * @param string $items[]['itemid']
2729	 * @param array  $items[]['tags']
2730	 * @param string $items[]['tags'][]['tag']
2731	 * @param string $items[]['tags'][]['value']
2732	 */
2733	protected function updateItemTags(array $items): void {
2734		$items = array_filter($items, function ($item) {
2735			return array_key_exists('tags', $item);
2736		});
2737
2738		// Select tags from database.
2739		$db_tags = DBselect(
2740			'SELECT itemtagid, itemid, tag, value'.
2741			' FROM item_tag'.
2742			' WHERE '.dbConditionInt('itemid', array_keys($items))
2743		);
2744
2745		array_walk($items, function (&$item) {
2746			$item['db_tags'] = [];
2747		});
2748
2749		while ($db_tag = DBfetch($db_tags)) {
2750			$items[$db_tag['itemid']]['db_tags'][] = $db_tag;
2751		}
2752
2753		// Find which tags must be added/deleted.
2754		$new_tags = [];
2755		$del_tagids = [];
2756		foreach ($items as $item) {
2757			CArrayHelper::sort($item['tags'], ['tag', 'value']);
2758
2759			foreach ($item['db_tags'] as $del_tag_key => $tag_delete) {
2760				foreach ($item['tags'] as $new_tag_key => $tag_add) {
2761					if ($tag_delete['tag'] === $tag_add['tag'] && $tag_delete['value'] === $tag_add['value']) {
2762						unset($item['db_tags'][$del_tag_key], $item['tags'][$new_tag_key]);
2763						continue 2;
2764					}
2765				}
2766			}
2767
2768			$del_tagids = array_merge($del_tagids, array_column($item['db_tags'], 'itemtagid'));
2769
2770			foreach ($item['tags'] as $tag_add) {
2771				$tag_add['itemid'] = $item['itemid'];
2772				$new_tags[] = $tag_add;
2773			}
2774		}
2775
2776		if ($del_tagids) {
2777			DB::delete('item_tag', ['itemtagid' => $del_tagids]);
2778		}
2779		if ($new_tags) {
2780			DB::insert('item_tag', $new_tags);
2781		}
2782	}
2783
2784	/**
2785	 * Record item tags into database.
2786	 *
2787	 * @param array  $items
2788	 * @param array  $items[]['tags']
2789	 * @param string $items[]['tags'][]['tag']
2790	 * @param string $items[]['tags'][]['value']
2791	 * @param int    $items[]['itemid']
2792	 */
2793	protected function createItemTags(array $items): void {
2794		$new_tags = [];
2795		foreach ($items as $key => $item) {
2796			if (array_key_exists('tags', $item)) {
2797				foreach ($item['tags'] as $tag) {
2798					$tag['itemid'] = $item['itemid'];
2799					$new_tags[] = $tag;
2800				}
2801			}
2802		}
2803
2804		if ($new_tags) {
2805			DB::insert('item_tag', $new_tags);
2806		}
2807	}
2808
2809	/**
2810	 * Check that valuemap belong to same host as item.
2811	 *
2812	 * @param array $items
2813	 */
2814	protected function validateValueMaps(array $items): void {
2815		$valuemapids_by_hostid = [];
2816
2817		foreach ($items as $item) {
2818			if (array_key_exists('valuemapid', $item) && $item['valuemapid'] != 0) {
2819				$valuemapids_by_hostid[$item['hostid']][$item['valuemapid']] = true;
2820			}
2821		}
2822
2823		$sql_where = [];
2824		foreach ($valuemapids_by_hostid as $hostid => $valuemapids) {
2825			$sql_where[] = '(vm.hostid='.zbx_dbstr($hostid).' AND '.
2826				dbConditionId('vm.valuemapid', array_keys($valuemapids)).')';
2827		}
2828
2829		if ($sql_where) {
2830			$result = DBselect(
2831				'SELECT vm.valuemapid,vm.hostid'.
2832				' FROM valuemap vm'.
2833				' WHERE '.implode(' OR ', $sql_where)
2834			);
2835			while ($row = DBfetch($result)) {
2836				unset($valuemapids_by_hostid[$row['hostid']][$row['valuemapid']]);
2837
2838				if (!$valuemapids_by_hostid[$row['hostid']]) {
2839					unset($valuemapids_by_hostid[$row['hostid']]);
2840				}
2841			}
2842
2843			if ($valuemapids_by_hostid) {
2844				$hostid = key($valuemapids_by_hostid);
2845				$valuemapid = key($valuemapids_by_hostid[$hostid]);
2846
2847				$host_row = DBfetch(DBselect('SELECT h.host FROM hosts h WHERE h.hostid='.zbx_dbstr($hostid)));
2848				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Valuemap with ID "%1$s" is not available on "%2$s".',
2849					$valuemapid, $host_row['host']
2850				));
2851			}
2852		}
2853	}
2854}
2855