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 discovery rules.
24 */
25class CDiscoveryRule extends CItemGeneral {
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		'copy' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN]
33	];
34
35	protected $tableName = 'items';
36	protected $tableAlias = 'i';
37	protected $sortColumns = ['itemid', 'name', 'key_', 'delay', 'type', 'status'];
38
39	/**
40	 * Define a set of supported pre-processing rules.
41	 *
42	 * @var array
43	 */
44	const SUPPORTED_PREPROCESSING_TYPES = [ZBX_PREPROC_REGSUB, ZBX_PREPROC_JSONPATH,
45		ZBX_PREPROC_VALIDATE_NOT_REGEX, ZBX_PREPROC_ERROR_FIELD_JSON, ZBX_PREPROC_THROTTLE_TIMED_VALUE,
46		ZBX_PREPROC_SCRIPT, ZBX_PREPROC_PROMETHEUS_TO_JSON, ZBX_PREPROC_XPATH, ZBX_PREPROC_ERROR_FIELD_XML,
47		ZBX_PREPROC_CSV_TO_JSON, ZBX_PREPROC_STR_REPLACE, ZBX_PREPROC_XML_TO_JSON
48	];
49
50	/**
51	 * Define a set of supported item types.
52	 *
53	 * @var array
54	 */
55	const SUPPORTED_ITEM_TYPES = [ITEM_TYPE_ZABBIX, ITEM_TYPE_TRAPPER, ITEM_TYPE_SIMPLE, ITEM_TYPE_INTERNAL,
56		ITEM_TYPE_ZABBIX_ACTIVE, ITEM_TYPE_EXTERNAL, ITEM_TYPE_DB_MONITOR, ITEM_TYPE_IPMI, ITEM_TYPE_SSH,
57		ITEM_TYPE_TELNET, ITEM_TYPE_JMX, ITEM_TYPE_DEPENDENT, ITEM_TYPE_HTTPAGENT, ITEM_TYPE_SNMP, ITEM_TYPE_SCRIPT
58	];
59
60	public function __construct() {
61		parent::__construct();
62
63		$this->errorMessages = array_merge($this->errorMessages, [
64			self::ERROR_EXISTS_TEMPLATE => _('Discovery rule "%1$s" already exists on "%2$s", inherited from another template.'),
65			self::ERROR_EXISTS => _('Discovery rule "%1$s" already exists on "%2$s".'),
66			self::ERROR_INVALID_KEY => _('Invalid key "%1$s" for discovery rule "%2$s" on "%3$s": %4$s.')
67		]);
68	}
69
70	/**
71	 * Get DiscoveryRule data
72	 */
73	public function get($options = []) {
74		$result = [];
75
76		$sqlParts = [
77			'select'	=> ['items' => 'i.itemid'],
78			'from'		=> ['items' => 'items i'],
79			'where'		=> ['i.flags='.ZBX_FLAG_DISCOVERY_RULE],
80			'group'		=> [],
81			'order'		=> [],
82			'limit'		=> null
83		];
84
85		$defOptions = [
86			'groupids'						=> null,
87			'templateids'					=> null,
88			'hostids'						=> null,
89			'itemids'						=> null,
90			'interfaceids'					=> null,
91			'inherited'						=> null,
92			'templated'						=> null,
93			'monitored'						=> null,
94			'editable'						=> false,
95			'nopermissions'					=> null,
96			// filter
97			'filter'						=> null,
98			'search'						=> null,
99			'searchByAny'					=> null,
100			'startSearch'					=> false,
101			'excludeSearch'					=> false,
102			'searchWildcardsEnabled'		=> null,
103			// output
104			'output'						=> API_OUTPUT_EXTEND,
105			'selectHosts'					=> null,
106			'selectItems'					=> null,
107			'selectTriggers'				=> null,
108			'selectGraphs'					=> null,
109			'selectHostPrototypes'			=> null,
110			'selectFilter'					=> null,
111			'selectLLDMacroPaths'			=> null,
112			'selectPreprocessing'			=> null,
113			'selectOverrides'				=> null,
114			'countOutput'					=> false,
115			'groupCount'					=> false,
116			'preservekeys'					=> false,
117			'sortfield'						=> '',
118			'sortorder'						=> '',
119			'limit'							=> null,
120			'limitSelects'					=> null
121		];
122		$options = zbx_array_merge($defOptions, $options);
123
124		// editable + PERMISSION CHECK
125		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN && !$options['nopermissions']) {
126			$permission = $options['editable'] ? PERM_READ_WRITE : PERM_READ;
127			$userGroups = getUserGroupsByUserId(self::$userData['userid']);
128
129			$sqlParts['where'][] = 'EXISTS ('.
130				'SELECT NULL'.
131				' FROM hosts_groups hgg'.
132					' JOIN rights r'.
133						' ON r.id=hgg.groupid'.
134							' AND '.dbConditionInt('r.groupid', $userGroups).
135				' WHERE i.hostid=hgg.hostid'.
136				' GROUP BY hgg.hostid'.
137				' HAVING MIN(r.permission)>'.PERM_DENY.
138					' AND MAX(r.permission)>='.zbx_dbstr($permission).
139				')';
140		}
141
142		// templateids
143		if (!is_null($options['templateids'])) {
144			zbx_value2array($options['templateids']);
145
146			if (!is_null($options['hostids'])) {
147				zbx_value2array($options['hostids']);
148				$options['hostids'] = array_merge($options['hostids'], $options['templateids']);
149			}
150			else {
151				$options['hostids'] = $options['templateids'];
152			}
153		}
154
155		// hostids
156		if (!is_null($options['hostids'])) {
157			zbx_value2array($options['hostids']);
158
159			$sqlParts['where']['hostid'] = dbConditionInt('i.hostid', $options['hostids']);
160
161			if ($options['groupCount']) {
162				$sqlParts['group']['i'] = 'i.hostid';
163			}
164		}
165
166		// itemids
167		if (!is_null($options['itemids'])) {
168			zbx_value2array($options['itemids']);
169
170			$sqlParts['where']['itemid'] = dbConditionInt('i.itemid', $options['itemids']);
171		}
172
173		// interfaceids
174		if (!is_null($options['interfaceids'])) {
175			zbx_value2array($options['interfaceids']);
176
177			$sqlParts['where']['interfaceid'] = dbConditionId('i.interfaceid', $options['interfaceids']);
178
179			if ($options['groupCount']) {
180				$sqlParts['group']['i'] = 'i.interfaceid';
181			}
182		}
183
184		// groupids
185		if ($options['groupids'] !== null) {
186			zbx_value2array($options['groupids']);
187
188			$sqlParts['from']['hosts_groups'] = 'hosts_groups hg';
189			$sqlParts['where'][] = dbConditionInt('hg.groupid', $options['groupids']);
190			$sqlParts['where'][] = 'hg.hostid=i.hostid';
191
192			if ($options['groupCount']) {
193				$sqlParts['group']['hg'] = 'hg.groupid';
194			}
195		}
196
197		// inherited
198		if (!is_null($options['inherited'])) {
199			if ($options['inherited']) {
200				$sqlParts['where'][] = 'i.templateid IS NOT NULL';
201			}
202			else {
203				$sqlParts['where'][] = 'i.templateid IS NULL';
204			}
205		}
206
207		// templated
208		if (!is_null($options['templated'])) {
209			$sqlParts['from']['hosts'] = 'hosts h';
210			$sqlParts['where']['hi'] = 'h.hostid=i.hostid';
211
212			if ($options['templated']) {
213				$sqlParts['where'][] = 'h.status='.HOST_STATUS_TEMPLATE;
214			}
215			else {
216				$sqlParts['where'][] = 'h.status<>'.HOST_STATUS_TEMPLATE;
217			}
218		}
219
220		// monitored
221		if (!is_null($options['monitored'])) {
222			$sqlParts['from']['hosts'] = 'hosts h';
223			$sqlParts['where']['hi'] = 'h.hostid=i.hostid';
224
225			if ($options['monitored']) {
226				$sqlParts['where'][] = 'h.status='.HOST_STATUS_MONITORED;
227				$sqlParts['where'][] = 'i.status='.ITEM_STATUS_ACTIVE;
228			}
229			else {
230				$sqlParts['where'][] = '(h.status<>'.HOST_STATUS_MONITORED.' OR i.status<>'.ITEM_STATUS_ACTIVE.')';
231			}
232		}
233
234		// search
235		if (is_array($options['search'])) {
236			if (array_key_exists('error', $options['search']) && $options['search']['error'] !== null) {
237				zbx_db_search('item_rtdata ir', ['search' => ['error' => $options['search']['error']]] + $options,
238					$sqlParts
239				);
240			}
241
242			zbx_db_search('items i', $options, $sqlParts);
243		}
244
245		// filter
246		if (is_array($options['filter'])) {
247			if (array_key_exists('delay', $options['filter']) && $options['filter']['delay'] !== null) {
248				$sqlParts['where'][] = makeUpdateIntervalFilter('i.delay', $options['filter']['delay']);
249				unset($options['filter']['delay']);
250			}
251
252			if (array_key_exists('lifetime', $options['filter']) && $options['filter']['lifetime'] !== null) {
253				$options['filter']['lifetime'] = getTimeUnitFilters($options['filter']['lifetime']);
254			}
255
256			if (array_key_exists('state', $options['filter']) && $options['filter']['state'] !== null) {
257				$this->dbFilter('item_rtdata ir', ['filter' => ['state' => $options['filter']['state']]] + $options,
258					$sqlParts
259				);
260			}
261
262			$this->dbFilter('items i', $options, $sqlParts);
263
264			if (isset($options['filter']['host'])) {
265				zbx_value2array($options['filter']['host']);
266
267				$sqlParts['from']['hosts'] = 'hosts h';
268				$sqlParts['where']['hi'] = 'h.hostid=i.hostid';
269				$sqlParts['where']['h'] = dbConditionString('h.host', $options['filter']['host']);
270			}
271		}
272
273		// limit
274		if (zbx_ctype_digit($options['limit']) && $options['limit']) {
275			$sqlParts['limit'] = $options['limit'];
276		}
277
278		$sqlParts = $this->applyQueryOutputOptions($this->tableName(), $this->tableAlias(), $options, $sqlParts);
279		$sqlParts = $this->applyQuerySortOptions($this->tableName(), $this->tableAlias(), $options, $sqlParts);
280		$res = DBselect(self::createSelectQueryFromParts($sqlParts), $sqlParts['limit']);
281		while ($item = DBfetch($res)) {
282			if (!$options['countOutput']) {
283				$result[$item['itemid']] = $item;
284				continue;
285			}
286
287			if ($options['groupCount']) {
288				$result[] = $item;
289			}
290			else {
291				$result = $item['rowscount'];
292			}
293		}
294
295		if ($options['countOutput']) {
296			return $result;
297		}
298
299		if ($result) {
300			if (self::dbDistinct($sqlParts)) {
301				$result = $this->addNclobFieldValues($options, $result);
302			}
303
304			$result = $this->addRelatedObjects($options, $result);
305			$result = $this->unsetExtraFields($result, ['hostid'], $options['output']);
306
307			foreach ($result as &$rule) {
308				// unset the fields that are returned in the filter
309				unset($rule['formula'], $rule['evaltype']);
310
311				if ($options['selectFilter'] !== null) {
312					$filter = $this->unsetExtraFields([$rule['filter']],
313						['conditions', 'formula', 'evaltype'],
314						$options['selectFilter']
315					);
316					$filter = reset($filter);
317					if (isset($filter['conditions'])) {
318						foreach ($filter['conditions'] as &$condition) {
319							unset($condition['item_conditionid'], $condition['itemid']);
320						}
321						unset($condition);
322					}
323
324					$rule['filter'] = $filter;
325				}
326			}
327			unset($rule);
328		}
329
330		// Decode ITEM_TYPE_HTTPAGENT encoded fields.
331		foreach ($result as &$item) {
332			if (array_key_exists('query_fields', $item)) {
333				$query_fields = ($item['query_fields'] !== '') ? json_decode($item['query_fields'], true) : [];
334				$item['query_fields'] = json_last_error() ? [] : $query_fields;
335			}
336
337			if (array_key_exists('headers', $item)) {
338				$item['headers'] = $this->headersStringToArray($item['headers']);
339			}
340
341			// Option 'Convert to JSON' is not supported for discovery rule.
342			unset($item['output_format']);
343		}
344		unset($item);
345
346		if (!$options['preservekeys']) {
347			$result = zbx_cleanHashes($result);
348		}
349
350		return $result;
351	}
352
353	/**
354	 * Add DiscoveryRule.
355	 *
356	 * @param array $items
357	 *
358	 * @return array
359	 */
360	public function create($items) {
361		$items = zbx_toArray($items);
362		$this->checkInput($items);
363
364		foreach ($items as &$item) {
365			if ($item['type'] == ITEM_TYPE_HTTPAGENT) {
366				if (array_key_exists('query_fields', $item)) {
367					$item['query_fields'] = $item['query_fields'] ? json_encode($item['query_fields']) : '';
368				}
369
370				if (array_key_exists('headers', $item)) {
371					$item['headers'] = $this->headersArrayToString($item['headers']);
372				}
373
374				if (array_key_exists('request_method', $item) && $item['request_method'] == HTTPCHECK_REQUEST_HEAD
375						&& !array_key_exists('retrieve_mode', $item)) {
376					$item['retrieve_mode'] = HTTPTEST_STEP_RETRIEVE_MODE_HEADERS;
377				}
378			}
379			else {
380				$item['query_fields'] = '';
381				$item['headers'] = '';
382			}
383
384			// Option 'Convert to JSON' is not supported for discovery rule.
385			unset($item['itemid'], $item['output_format']);
386		}
387		unset($item);
388
389		// Get only hosts not templates from items
390		$hosts = API::Host()->get([
391			'output' => [],
392			'hostids' => zbx_objectValues($items, 'hostid'),
393			'preservekeys' => true
394		]);
395		foreach ($items as &$item) {
396			if (array_key_exists($item['hostid'], $hosts)) {
397				$item['rtdata'] = true;
398			}
399		}
400		unset($item);
401
402		$this->validateCreateLLDMacroPaths($items);
403		$this->validateDependentItems($items);
404		$this->createReal($items);
405		$this->inherit($items);
406
407		return ['itemids' => zbx_objectValues($items, 'itemid')];
408	}
409
410	/**
411	 * Update DiscoveryRule.
412	 *
413	 * @param array $items
414	 *
415	 * @return array
416	 */
417	public function update($items) {
418		$items = zbx_toArray($items);
419
420		$db_items = $this->get([
421			'output' => ['itemid', 'name', 'type', 'master_itemid', 'authtype', 'allow_traps', 'retrieve_mode'],
422			'selectFilter' => ['evaltype', 'formula', 'conditions'],
423			'itemids' => zbx_objectValues($items, 'itemid'),
424			'preservekeys' => true
425		]);
426
427		$this->checkInput($items, true, $db_items);
428		$this->validateUpdateLLDMacroPaths($items);
429
430		$items = $this->extendFromObjects(zbx_toHash($items, 'itemid'), $db_items, ['flags', 'type', 'authtype',
431			'master_itemid'
432		]);
433		$this->validateDependentItems($items);
434
435		$defaults = DB::getDefaults('items');
436		$clean = [
437			ITEM_TYPE_HTTPAGENT => [
438				'url' => '',
439				'query_fields' => '',
440				'timeout' => $defaults['timeout'],
441				'status_codes' => $defaults['status_codes'],
442				'follow_redirects' => $defaults['follow_redirects'],
443				'request_method' => $defaults['request_method'],
444				'allow_traps' => $defaults['allow_traps'],
445				'post_type' => $defaults['post_type'],
446				'http_proxy' => '',
447				'headers' => '',
448				'retrieve_mode' => $defaults['retrieve_mode'],
449				'output_format' => $defaults['output_format'],
450				'ssl_key_password' => '',
451				'verify_peer' => $defaults['verify_peer'],
452				'verify_host' => $defaults['verify_host'],
453				'ssl_cert_file' => '',
454				'ssl_key_file' => '',
455				'posts' => ''
456			]
457		];
458
459		// set the default values required for updating
460		foreach ($items as &$item) {
461			$type_change = (array_key_exists('type', $item) && $item['type'] != $db_items[$item['itemid']]['type']);
462
463			if (isset($item['filter'])) {
464				foreach ($item['filter']['conditions'] as &$condition) {
465					$condition += [
466						'operator' => DB::getDefault('item_condition', 'operator')
467					];
468				}
469				unset($condition);
470			}
471
472			if ($type_change && $db_items[$item['itemid']]['type'] == ITEM_TYPE_HTTPAGENT) {
473				$item = array_merge($item, $clean[ITEM_TYPE_HTTPAGENT]);
474
475				if ($item['type'] != ITEM_TYPE_SSH) {
476					$item['authtype'] = $defaults['authtype'];
477					$item['username'] = '';
478					$item['password'] = '';
479				}
480
481				if ($item['type'] != ITEM_TYPE_TRAPPER) {
482					$item['trapper_hosts'] = '';
483				}
484			}
485
486			if ($item['type'] == ITEM_TYPE_HTTPAGENT) {
487				// Clean username and password when authtype is set to HTTPTEST_AUTH_NONE.
488				if ($item['authtype'] == HTTPTEST_AUTH_NONE) {
489					$item['username'] = '';
490					$item['password'] = '';
491				}
492
493				if (array_key_exists('allow_traps', $item) && $item['allow_traps'] == HTTPCHECK_ALLOW_TRAPS_OFF
494						&& $item['allow_traps'] != $db_items[$item['itemid']]['allow_traps']) {
495					$item['trapper_hosts'] = '';
496				}
497
498				if (array_key_exists('query_fields', $item) && is_array($item['query_fields'])) {
499					$item['query_fields'] = $item['query_fields'] ? json_encode($item['query_fields']) : '';
500				}
501
502				if (array_key_exists('headers', $item) && is_array($item['headers'])) {
503					$item['headers'] = $this->headersArrayToString($item['headers']);
504				}
505
506				if (array_key_exists('request_method', $item) && $item['request_method'] == HTTPCHECK_REQUEST_HEAD
507						&& !array_key_exists('retrieve_mode', $item)
508						&& $db_items[$item['itemid']]['retrieve_mode'] != HTTPTEST_STEP_RETRIEVE_MODE_HEADERS) {
509					$item['retrieve_mode'] = HTTPTEST_STEP_RETRIEVE_MODE_HEADERS;
510				}
511			}
512			else {
513				$item['query_fields'] = '';
514				$item['headers'] = '';
515			}
516
517			if ($type_change && $db_items[$item['itemid']]['type'] == ITEM_TYPE_SCRIPT) {
518				if ($item['type'] != ITEM_TYPE_SSH && $item['type'] != ITEM_TYPE_DB_MONITOR
519						&& $item['type'] != ITEM_TYPE_TELNET && $item['type'] != ITEM_TYPE_CALCULATED) {
520					$item['params'] = '';
521				}
522
523				if ($item['type'] != ITEM_TYPE_HTTPAGENT) {
524					$item['timeout'] = $defaults['timeout'];
525				}
526			}
527
528			// Option 'Convert to JSON' is not supported for discovery rule.
529			unset($item['output_format']);
530		}
531		unset($item);
532
533		// update
534		$this->updateReal($items);
535		$this->inherit($items);
536
537		return ['itemids' => zbx_objectValues($items, 'itemid')];
538	}
539
540	/**
541	 * Delete DiscoveryRules.
542	 *
543	 * @param array $ruleids
544	 *
545	 * @return array
546	 */
547	public function delete(array $ruleids) {
548		$this->validateDelete($ruleids);
549
550		CDiscoveryRuleManager::delete($ruleids);
551
552		return ['ruleids' => $ruleids];
553	}
554
555	/**
556	 * Validates the input parameters for the delete() method.
557	 *
558	 * @param array $ruleids   [IN/OUT]
559	 *
560	 * @throws APIException if the input is invalid.
561	 */
562	private function validateDelete(array &$ruleids) {
563		$api_input_rules = ['type' => API_IDS, 'flags' => API_NOT_EMPTY, 'uniq' => true];
564		if (!CApiInputValidator::validate($api_input_rules, $ruleids, '/', $error)) {
565			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
566		}
567
568		$db_rules = $this->get([
569			'output' => ['templateid'],
570			'itemids' => $ruleids,
571			'editable' => true,
572			'preservekeys' => true
573		]);
574
575		foreach ($ruleids as $ruleid) {
576			if (!array_key_exists($ruleid, $db_rules)) {
577				self::exception(ZBX_API_ERROR_PERMISSIONS,
578					_('No permissions to referred object or it does not exist!')
579				);
580			}
581
582			$db_rule = $db_rules[$ruleid];
583
584			if ($db_rule['templateid'] != 0) {
585				self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot delete templated items.'));
586			}
587		}
588	}
589
590	/**
591	 * Checks if the current user has access to the given hosts and templates. Assumes the "hostid" field is valid.
592	 *
593	 * @param array $hostids    an array of host or template IDs
594	 *
595	 * @throws APIException if the user doesn't have write permissions for the given hosts.
596	 */
597	protected function checkHostPermissions(array $hostids) {
598		if ($hostids) {
599			$hostids = array_unique($hostids);
600
601			$count = API::Host()->get([
602				'countOutput' => true,
603				'hostids' => $hostids,
604				'editable' => true
605			]);
606
607			if ($count == count($hostids)) {
608				return;
609			}
610
611			$count += API::Template()->get([
612				'countOutput' => true,
613				'templateids' => $hostids,
614				'editable' => true
615			]);
616
617			if ($count != count($hostids)) {
618				self::exception(ZBX_API_ERROR_PERMISSIONS,
619					_('No permissions to referred object or it does not exist!')
620				);
621			}
622		}
623	}
624
625	/**
626	 * Copies the given discovery rules to the specified hosts.
627	 *
628	 * @throws APIException if no discovery rule IDs or host IDs are given or
629	 * the user doesn't have the necessary permissions.
630	 *
631	 * @param array $data
632	 * @param array $data['discoveryids']  An array of item ids to be cloned.
633	 * @param array $data['hostids']       An array of host ids were the items should be cloned to.
634	 *
635	 * @return bool
636	 */
637	public function copy(array $data) {
638		// validate data
639		if (!isset($data['discoveryids']) || !$data['discoveryids']) {
640			self::exception(ZBX_API_ERROR_PARAMETERS, _('No discovery rule IDs given.'));
641		}
642		if (!isset($data['hostids']) || !$data['hostids']) {
643			self::exception(ZBX_API_ERROR_PARAMETERS, _('No host IDs given.'));
644		}
645
646		$this->checkHostPermissions($data['hostids']);
647
648		// check if the given discovery rules exist
649		$count = $this->get([
650			'countOutput' => true,
651			'itemids' => $data['discoveryids']
652		]);
653
654		if ($count != count($data['discoveryids'])) {
655			self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
656		}
657
658		// copy
659		foreach ($data['discoveryids'] as $discoveryid) {
660			foreach ($data['hostids'] as $hostid) {
661				$this->copyDiscoveryRule($discoveryid, $hostid);
662			}
663		}
664
665		return true;
666	}
667
668	public function syncTemplates($data) {
669		$data['templateids'] = zbx_toArray($data['templateids']);
670		$data['hostids'] = zbx_toArray($data['hostids']);
671
672		$output = [];
673		foreach ($this->fieldRules as $field_name => $rules) {
674			if (!array_key_exists('system', $rules) && !array_key_exists('host', $rules)) {
675				$output[] = $field_name;
676			}
677		}
678
679		$tpl_items = $this->get([
680			'output' => $output,
681			'hostids' => $data['templateids'],
682			'selectFilter' => ['formula', 'evaltype', 'conditions'],
683			'selectLLDMacroPaths' => ['lld_macro', 'path'],
684			'selectPreprocessing' => ['type', 'params', 'error_handler', 'error_handler_params'],
685			'selectOverrides' => ['name', 'step', 'stop', 'filter', 'operations'],
686			'preservekeys' => true
687		]);
688
689		foreach ($tpl_items as &$item) {
690			if ($item['type'] == ITEM_TYPE_HTTPAGENT) {
691				if (array_key_exists('query_fields', $item) && is_array($item['query_fields'])) {
692					$item['query_fields'] = $item['query_fields'] ? json_encode($item['query_fields']) : '';
693				}
694
695				if (array_key_exists('headers', $item) && is_array($item['headers'])) {
696					$item['headers'] = $this->headersArrayToString($item['headers']);
697				}
698			}
699			else {
700				$item['query_fields'] = '';
701				$item['headers'] = '';
702			}
703
704			// Option 'Convert to JSON' is not supported for discovery rule.
705			unset($item['output_format']);
706		}
707		unset($item);
708
709		$this->inherit($tpl_items, $data['hostids']);
710
711		return true;
712	}
713
714	/**
715	 * Copies all of the triggers from the source discovery to the target discovery rule.
716	 *
717	 * @throws APIException if trigger saving fails
718	 *
719	 * @param array $srcDiscovery    The source discovery rule to copy from
720	 * @param array $srcHost         The host the source discovery belongs to
721	 * @param array $dstHost         The host the target discovery belongs to
722	 *
723	 * @return array
724	 */
725	protected function copyTriggerPrototypes(array $srcDiscovery, array $srcHost, array $dstHost) {
726		$srcTriggers = API::TriggerPrototype()->get([
727			'discoveryids' => $srcDiscovery['itemid'],
728			'output' => ['triggerid', 'expression', 'description', 'url', 'status', 'priority', 'comments',
729				'templateid', 'type', 'recovery_mode', 'recovery_expression', 'correlation_mode', 'correlation_tag',
730				'opdata', 'discover', 'event_name'
731			],
732			'selectHosts' => API_OUTPUT_EXTEND,
733			'selectItems' => ['itemid', 'type'],
734			'selectDiscoveryRule' => API_OUTPUT_EXTEND,
735			'selectFunctions' => API_OUTPUT_EXTEND,
736			'selectDependencies' => ['triggerid'],
737			'selectTags' => ['tag', 'value'],
738			'preservekeys' => true
739		]);
740
741		foreach ($srcTriggers as $id => $trigger) {
742			// Skip trigger prototypes with web items and remove them from source.
743			if (httpItemExists($trigger['items'])) {
744				unset($srcTriggers[$id]);
745			}
746		}
747
748		if (!$srcTriggers) {
749			return [];
750		}
751
752		/*
753		 * Copy the remaining trigger prototypes to a new source. These will contain IDs and original dependencies.
754		 * The dependencies from $srcTriggers will be removed.
755		 */
756		$trigger_prototypes = $srcTriggers;
757
758		// Contains original trigger prototype dependency IDs.
759		$dep_triggerids = [];
760
761		/*
762		 * Collect dependency trigger IDs and remove them from source. Otherwise these IDs do not pass
763		 * validation, since they don't belong to destination discovery rule.
764		 */
765		$add_dependencies = false;
766
767		foreach ($srcTriggers as $id => &$trigger) {
768			if ($trigger['dependencies']) {
769				foreach ($trigger['dependencies'] as $dep_trigger) {
770					$dep_triggerids[] = $dep_trigger['triggerid'];
771				}
772				$add_dependencies = true;
773			}
774			unset($trigger['dependencies']);
775		}
776		unset($trigger);
777
778		// Save new trigger prototypes and without dependencies for now.
779		$dstTriggers = $srcTriggers;
780		$dstTriggers = CMacrosResolverHelper::resolveTriggerExpressions($dstTriggers,
781			['sources' => ['expression', 'recovery_expression']]
782		);
783		foreach ($dstTriggers as $id => &$trigger) {
784			unset($trigger['triggerid'], $trigger['templateid'], $trigger['hosts'], $trigger['functions'],
785				$trigger['items'], $trigger['discoveryRule']
786			);
787
788			// Update the destination expressions.
789			$trigger['expression'] = triggerExpressionReplaceHost($trigger['expression'], $srcHost['host'],
790				$dstHost['host']
791			);
792			if ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) {
793				$trigger['recovery_expression'] = triggerExpressionReplaceHost($trigger['recovery_expression'],
794					$srcHost['host'], $dstHost['host']
795				);
796			}
797		}
798		unset($trigger);
799
800		$result = API::TriggerPrototype()->create($dstTriggers);
801		if (!$result) {
802			self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot clone trigger prototypes.'));
803		}
804
805		// Process dependencies, if at least one trigger prototype has a dependency.
806		if ($add_dependencies) {
807			$trigger_prototypeids = array_keys($trigger_prototypes);
808
809			foreach ($result['triggerids'] as $i => $triggerid) {
810				$new_trigger_prototypes[$trigger_prototypeids[$i]] = [
811					'new_triggerid' => $triggerid,
812					'new_hostid' => $dstHost['hostid'],
813					'new_host' => $dstHost['host'],
814					'src_hostid' => $srcHost['hostid'],
815					'src_host' => $srcHost['host']
816				];
817			}
818
819			/*
820			 * Search for original dependent triggers and expressions to find corresponding triggers on destination host
821			 * with same expression.
822			 */
823			$dep_triggers = API::Trigger()->get([
824				'output' => ['description', 'expression'],
825				'selectHosts' => ['hostid'],
826				'triggerids' => $dep_triggerids,
827				'preservekeys' => true
828			]);
829			$dep_triggers = CMacrosResolverHelper::resolveTriggerExpressions($dep_triggers);
830
831			// Map dependencies to the new trigger IDs and save.
832			foreach ($trigger_prototypes as &$trigger_prototype) {
833				// Get corresponding created trigger prototype ID.
834				$new_trigger_prototype = $new_trigger_prototypes[$trigger_prototype['triggerid']];
835
836				if ($trigger_prototype['dependencies']) {
837					foreach ($trigger_prototype['dependencies'] as &$dependency) {
838						$dep_triggerid = $dependency['triggerid'];
839
840						/*
841						 * We have added a dependent trigger prototype and we know corresponding trigger prototype ID
842						 * for newly created trigger prototype.
843						 */
844						if (array_key_exists($dependency['triggerid'], $new_trigger_prototypes)) {
845							/*
846							 * Dependency is within same host according to $srcHostId parameter or dep trigger has
847							 * single host.
848							 */
849							if ($new_trigger_prototype['src_hostid'] ==
850									$new_trigger_prototypes[$dep_triggerid]['src_hostid']) {
851								$dependency['triggerid'] = $new_trigger_prototypes[$dep_triggerid]['new_triggerid'];
852							}
853						}
854						elseif (in_array(['hostid' => $new_trigger_prototype['src_hostid']],
855								$dep_triggers[$dep_triggerid]['hosts'])) {
856							// Get all possible $depTrigger matching triggers by description.
857							$target_triggers = API::Trigger()->get([
858								'output' => ['hosts', 'triggerid', 'expression'],
859								'hostids' => $new_trigger_prototype['new_hostid'],
860								'filter' => ['description' => $dep_triggers[$dep_triggerid]['description']],
861								'preservekeys' => true
862							]);
863							$target_triggers = CMacrosResolverHelper::resolveTriggerExpressions($target_triggers);
864
865							// Compare exploded expressions for exact match.
866							$expr1 = $dep_triggers[$dep_triggerid]['expression'];
867							$dependency['triggerid'] = null;
868
869							foreach ($target_triggers as $target_trigger) {
870								$expr2 = triggerExpressionReplaceHost($target_trigger['expression'],
871									$new_trigger_prototype['new_host'],
872									$new_trigger_prototype['src_host']
873								);
874
875								if ($expr2 === $expr1) {
876									// Matching trigger has been found.
877									$dependency['triggerid'] = $target_trigger['triggerid'];
878									break;
879								}
880							}
881
882							// If matching trigger was not found, raise exception.
883							if ($dependency['triggerid'] === null) {
884								$expr2 = triggerExpressionReplaceHost($dep_triggers[$dep_triggerid]['expression'],
885									$new_trigger_prototype['src_host'],
886									$new_trigger_prototype['new_host']
887								);
888								self::exception(ZBX_API_ERROR_PARAMETERS, _s(
889									'Cannot add dependency from trigger "%1$s:%2$s" to non existing trigger "%3$s:%4$s".',
890									$trigger_prototype['description'],
891									$trigger_prototype['expression'],
892									$dep_triggers[$dep_triggerid]['description'],
893									$expr2
894								));
895							}
896						}
897					}
898					unset($dependency);
899
900					$trigger_prototype['triggerid'] = $new_trigger_prototype['new_triggerid'];
901				}
902			}
903			unset($trigger_prototype);
904
905			// If adding a dependency fails, the exception will be raised in TriggerPrototype API.
906			API::TriggerPrototype()->addDependencies($trigger_prototypes);
907		}
908
909		return $result;
910	}
911
912	protected function createReal(array &$items) {
913		$items_rtdata = [];
914		$create_items = [];
915
916		// create items without formulas, they will be updated when items and conditions are saved
917		foreach ($items as $key => $item) {
918			if (array_key_exists('filter', $item)) {
919				$item['evaltype'] = $item['filter']['evaltype'];
920				unset($item['filter']);
921			}
922
923			if (array_key_exists('rtdata', $item)) {
924				$items_rtdata[$key] = [];
925				unset($item['rtdata']);
926			}
927
928			$create_items[] = $item;
929		}
930		$create_items = DB::save('items', $create_items);
931
932		foreach ($items_rtdata as $key => &$value) {
933			$value['itemid'] = $create_items[$key]['itemid'];
934		}
935		unset($value);
936
937		DB::insert('item_rtdata', $items_rtdata, false);
938
939		$conditions = [];
940		$itemids = [];
941
942		foreach ($items as $key => &$item) {
943			$item['itemid'] = $create_items[$key]['itemid'];
944			$itemids[$key] = $item['itemid'];
945
946			// conditions
947			if (isset($item['filter'])) {
948				foreach ($item['filter']['conditions'] as $condition) {
949					$condition['itemid'] = $item['itemid'];
950
951					$conditions[] = $condition;
952				}
953			}
954		}
955		unset($item);
956
957		$conditions = DB::save('item_condition', $conditions);
958
959		$item_conditions = [];
960
961		foreach ($conditions as $condition) {
962			$item_conditions[$condition['itemid']][] = $condition;
963		}
964
965		$lld_macro_paths = [];
966
967		foreach ($items as $item) {
968			// update formulas
969			if (isset($item['filter']) && $item['filter']['evaltype'] == CONDITION_EVAL_TYPE_EXPRESSION) {
970				$this->updateFormula($item['itemid'], $item['filter']['formula'], $item_conditions[$item['itemid']]);
971			}
972
973			// $item['lld_macro_paths'] expects to be filled with validated fields 'lld_macro' and 'path' and values.
974			if (array_key_exists('lld_macro_paths', $item)) {
975				foreach ($item['lld_macro_paths'] as $lld_macro_path) {
976					$lld_macro_paths[] = $lld_macro_path + ['itemid' => $item['itemid']];
977				}
978			}
979		}
980
981		DB::insertBatch('lld_macro_path', $lld_macro_paths);
982
983		$this->createItemParameters($items, $itemids);
984		$this->createItemPreprocessing($items);
985		$this->createOverrides($items);
986	}
987
988	/**
989	 * Creates overrides for low-level discovery rules.
990	 *
991	 * @param array $items  Low-level discovery rules.
992	 */
993	protected function createOverrides(array $items) {
994		$overrides = [];
995
996		foreach ($items as $item) {
997			if (array_key_exists('overrides', $item)) {
998				foreach ($item['overrides'] as $override) {
999					// Formula will be added after conditions.
1000					$new_override = [
1001						'itemid' => $item['itemid'],
1002						'name' => $override['name'],
1003						'step' => $override['step'],
1004						'stop' => array_key_exists('stop', $override) ? $override['stop'] : ZBX_LLD_OVERRIDE_STOP_NO
1005					];
1006
1007					$new_override['evaltype'] = array_key_exists('filter', $override)
1008						? $override['filter']['evaltype']
1009						: DB::getDefault('lld_override', 'evaltype');
1010
1011					$overrides[] = $new_override;
1012				}
1013			}
1014		}
1015
1016		$overrideids = DB::insertBatch('lld_override', $overrides);
1017
1018		if ($overrideids) {
1019			$ovrd_conditions = [];
1020			$ovrd_idx = 0;
1021			$cnd_idx = 0;
1022
1023			foreach ($items as &$item) {
1024				if (array_key_exists('overrides', $item)) {
1025					foreach ($item['overrides'] as &$override) {
1026						$override['lld_overrideid'] = $overrideids[$ovrd_idx++];
1027
1028						if (array_key_exists('filter', $override)) {
1029							foreach ($override['filter']['conditions'] as $condition) {
1030								$ovrd_conditions[] = [
1031									'macro' => $condition['macro'],
1032									'value' => $condition['value'],
1033									'formulaid' => array_key_exists('formulaid', $condition)
1034										? $condition['formulaid']
1035										: '',
1036									'operator' => array_key_exists('operator', $condition)
1037										? $condition['operator']
1038										: DB::getDefault('lld_override_condition', 'operator'),
1039									'lld_overrideid' => $override['lld_overrideid']
1040								];
1041							}
1042						}
1043					}
1044					unset($override);
1045				}
1046			}
1047			unset($item);
1048
1049			$conditionids = DB::insertBatch('lld_override_condition', $ovrd_conditions);
1050
1051			$ids = [];
1052
1053			if ($conditionids) {
1054				foreach ($items as &$item) {
1055					if (array_key_exists('overrides', $item)) {
1056						foreach ($item['overrides'] as &$override) {
1057							if (array_key_exists('filter', $override)) {
1058								foreach ($override['filter']['conditions'] as &$condition) {
1059									$condition['lld_override_conditionid'] = $conditionids[$cnd_idx++];
1060								}
1061								unset($condition);
1062							}
1063						}
1064						unset($override);
1065					}
1066				}
1067				unset($item);
1068
1069				foreach ($items as $item) {
1070					if (array_key_exists('overrides', $item)) {
1071						foreach ($item['overrides'] as $override) {
1072							if (array_key_exists('filter', $override)
1073									&& $override['filter']['evaltype'] == CONDITION_EVAL_TYPE_EXPRESSION) {
1074								$ids = [];
1075								foreach ($override['filter']['conditions'] as $condition) {
1076									$ids[$condition['formulaid']] = $condition['lld_override_conditionid'];
1077								}
1078
1079								$formula = CConditionHelper::replaceLetterIds($override['filter']['formula'], $ids);
1080								DB::updateByPk('lld_override', $override['lld_overrideid'], ['formula' => $formula]);
1081							}
1082						}
1083					}
1084				}
1085			}
1086
1087			$operations = [];
1088			foreach ($items as $item) {
1089				if (array_key_exists('overrides', $item)) {
1090					foreach ($item['overrides'] as $override) {
1091						if (array_key_exists('operations', $override)) {
1092							foreach ($override['operations'] as $operation) {
1093								$operations[] = [
1094									'lld_overrideid' => $override['lld_overrideid'],
1095									'operationobject' => $operation['operationobject'],
1096									'operator' => array_key_exists('operator', $operation)
1097										? $operation['operator']
1098										: DB::getDefault('lld_override_operation', 'operator'),
1099									'value' => array_key_exists('value', $operation) ? $operation['value'] : ''
1100								];
1101							}
1102						}
1103					}
1104				}
1105			}
1106
1107			$operationids = DB::insertBatch('lld_override_operation', $operations);
1108
1109			$opr_idx = 0;
1110			$opstatus = [];
1111			$opdiscover = [];
1112			$opperiod = [];
1113			$ophistory = [];
1114			$optrends = [];
1115			$opseverity = [];
1116			$optag = [];
1117			$optemplate = [];
1118			$opinventory = [];
1119
1120			foreach ($items as $item) {
1121				if (array_key_exists('overrides', $item)) {
1122					foreach ($item['overrides'] as $override) {
1123						if (array_key_exists('operations', $override)) {
1124							foreach ($override['operations'] as $operation) {
1125								$operation['lld_override_operationid'] = $operationids[$opr_idx++];
1126
1127								// Discover status applies to all operation object types.
1128								if (array_key_exists('opdiscover', $operation)) {
1129									$opdiscover[] = [
1130										'lld_override_operationid' => $operation['lld_override_operationid'],
1131										'discover' => $operation['opdiscover']['discover']
1132									];
1133								}
1134
1135								switch ($operation['operationobject']) {
1136									case OPERATION_OBJECT_ITEM_PROTOTYPE:
1137										if (array_key_exists('opstatus', $operation)) {
1138											$opstatus[] = [
1139												'lld_override_operationid' => $operation['lld_override_operationid'],
1140												'status' => $operation['opstatus']['status']
1141											];
1142										}
1143
1144										if (array_key_exists('opperiod', $operation)) {
1145											$opperiod[] = [
1146												'lld_override_operationid' => $operation['lld_override_operationid'],
1147												'delay' => $operation['opperiod']['delay']
1148											];
1149										}
1150
1151										if (array_key_exists('ophistory', $operation)) {
1152											$ophistory[] = [
1153												'lld_override_operationid' => $operation['lld_override_operationid'],
1154												'history' => $operation['ophistory']['history']
1155											];
1156										}
1157
1158										if (array_key_exists('optrends', $operation)) {
1159											$optrends[] = [
1160												'lld_override_operationid' => $operation['lld_override_operationid'],
1161												'trends' => $operation['optrends']['trends']
1162											];
1163										}
1164
1165										if (array_key_exists('optag', $operation)) {
1166											foreach ($operation['optag'] as $tag) {
1167												$optag[] = [
1168													'lld_override_operationid' =>
1169														$operation['lld_override_operationid'],
1170													'tag' => $tag['tag'],
1171													'value'	=> array_key_exists('value', $tag) ? $tag['value'] : ''
1172												];
1173											}
1174										}
1175										break;
1176
1177									case OPERATION_OBJECT_TRIGGER_PROTOTYPE:
1178										if (array_key_exists('opstatus', $operation)) {
1179											$opstatus[] = [
1180												'lld_override_operationid' => $operation['lld_override_operationid'],
1181												'status' => $operation['opstatus']['status']
1182											];
1183										}
1184
1185										if (array_key_exists('opseverity', $operation)) {
1186											$opseverity[] = [
1187												'lld_override_operationid' => $operation['lld_override_operationid'],
1188												'severity' => $operation['opseverity']['severity']
1189											];
1190										}
1191
1192										if (array_key_exists('optag', $operation)) {
1193											foreach ($operation['optag'] as $tag) {
1194												$optag[] = [
1195													'lld_override_operationid' =>
1196														$operation['lld_override_operationid'],
1197													'tag' => $tag['tag'],
1198													'value'	=> array_key_exists('value', $tag) ? $tag['value'] : ''
1199												];
1200											}
1201										}
1202										break;
1203
1204									case OPERATION_OBJECT_HOST_PROTOTYPE:
1205										if (array_key_exists('opstatus', $operation)) {
1206											$opstatus[] = [
1207												'lld_override_operationid' => $operation['lld_override_operationid'],
1208												'status' => $operation['opstatus']['status']
1209											];
1210										}
1211
1212										if (array_key_exists('optemplate', $operation)) {
1213											foreach ($operation['optemplate'] as $template) {
1214												$optemplate[] = [
1215													'lld_override_operationid' =>
1216														$operation['lld_override_operationid'],
1217													'templateid' => $template['templateid']
1218												];
1219											}
1220										}
1221
1222										if (array_key_exists('optag', $operation)) {
1223											foreach ($operation['optag'] as $tag) {
1224												$optag[] = [
1225													'lld_override_operationid' =>
1226														$operation['lld_override_operationid'],
1227													'tag' => $tag['tag'],
1228													'value'	=> array_key_exists('value', $tag) ? $tag['value'] : ''
1229												];
1230											}
1231										}
1232
1233										if (array_key_exists('opinventory', $operation)) {
1234											$opinventory[] = [
1235												'lld_override_operationid' => $operation['lld_override_operationid'],
1236												'inventory_mode' => $operation['opinventory']['inventory_mode']
1237											];
1238										}
1239										break;
1240								}
1241							}
1242						}
1243					}
1244				}
1245			}
1246
1247			DB::insertBatch('lld_override_opstatus', $opstatus, false);
1248			DB::insertBatch('lld_override_opdiscover', $opdiscover, false);
1249			DB::insertBatch('lld_override_opperiod', $opperiod, false);
1250			DB::insertBatch('lld_override_ophistory', $ophistory, false);
1251			DB::insertBatch('lld_override_optrends', $optrends, false);
1252			DB::insertBatch('lld_override_opseverity', $opseverity, false);
1253			DB::insertBatch('lld_override_optag', $optag);
1254			DB::insertBatch('lld_override_optemplate', $optemplate);
1255			DB::insertBatch('lld_override_opinventory', $opinventory, false);
1256		}
1257	}
1258
1259	protected function updateReal(array $items) {
1260		CArrayHelper::sort($items, ['itemid']);
1261
1262		$ruleIds = zbx_objectValues($items, 'itemid');
1263
1264		$data = [];
1265		foreach ($items as $item) {
1266			$values = $item;
1267
1268			if (isset($item['filter'])) {
1269				// clear the formula for non-custom expression rules
1270				if ($item['filter']['evaltype'] != CONDITION_EVAL_TYPE_EXPRESSION) {
1271					$values['formula'] = '';
1272				}
1273
1274				$values['evaltype'] = $item['filter']['evaltype'];
1275				unset($values['filter']);
1276			}
1277
1278			$data[] = ['values' => $values, 'where' => ['itemid' => $item['itemid']]];
1279		}
1280		DB::update('items', $data);
1281
1282		$newRuleConditions = null;
1283		foreach ($items as $item) {
1284			// conditions
1285			if (isset($item['filter'])) {
1286				if ($newRuleConditions === null) {
1287					$newRuleConditions = [];
1288				}
1289
1290				$newRuleConditions[$item['itemid']] = [];
1291				foreach ($item['filter']['conditions'] as $condition) {
1292					$condition['itemid'] = $item['itemid'];
1293
1294					$newRuleConditions[$item['itemid']][] = $condition;
1295				}
1296			}
1297		}
1298
1299		// replace conditions
1300		$ruleConditions = [];
1301		if ($newRuleConditions !== null) {
1302			// fetch existing conditions
1303			$exConditions = DBfetchArray(DBselect(
1304				'SELECT item_conditionid,itemid,macro,value,operator'.
1305				' FROM item_condition'.
1306				' WHERE '.dbConditionInt('itemid', $ruleIds).
1307				' ORDER BY item_conditionid'
1308			));
1309			$exRuleConditions = [];
1310			foreach ($exConditions as $condition) {
1311				$exRuleConditions[$condition['itemid']][] = $condition;
1312			}
1313
1314			// replace and add the new IDs
1315			$conditions = DB::replaceByPosition('item_condition', $exRuleConditions, $newRuleConditions);
1316			foreach ($conditions as $condition) {
1317				$ruleConditions[$condition['itemid']][] = $condition;
1318			}
1319		}
1320
1321		$itemids = [];
1322		$lld_macro_paths = [];
1323		$db_lld_macro_paths = [];
1324
1325		foreach ($items as $item) {
1326			// update formulas
1327			if (isset($item['filter']) && $item['filter']['evaltype'] == CONDITION_EVAL_TYPE_EXPRESSION) {
1328				$this->updateFormula($item['itemid'], $item['filter']['formula'], $ruleConditions[$item['itemid']]);
1329			}
1330
1331			// "lld_macro_paths" could be empty or filled with fields "lld_macro", "path" or "lld_macro_pathid".
1332			if (array_key_exists('lld_macro_paths', $item)) {
1333				$itemids[$item['itemid']] = true;
1334
1335				if ($item['lld_macro_paths']) {
1336					foreach ($item['lld_macro_paths'] as $lld_macro_path) {
1337						$lld_macro_paths[] = $lld_macro_path + ['itemid' => $item['itemid']];
1338					}
1339				}
1340			}
1341		}
1342
1343		// Gather all existing LLD macros from given discovery rules.
1344		if ($itemids) {
1345			$db_lld_macro_paths = DB::select('lld_macro_path', [
1346				'output' => ['lld_macro_pathid', 'itemid', 'lld_macro', 'path'],
1347				'filter' => ['itemid' => array_keys($itemids)]
1348			]);
1349		}
1350
1351		/*
1352		 * DB::replaceByPosition() does not allow to change records one by one due to unique indexes on two table
1353		 * columns. Problems arise when given records are the same as records in DB and they are sorted differently.
1354		 * That's why checking differences between old and new records is done manually.
1355		 */
1356
1357		$lld_macro_paths_to_update = [];
1358
1359		foreach ($lld_macro_paths as $idx1 => $lld_macro_path) {
1360			foreach ($db_lld_macro_paths as $idx2 => $db_lld_macro_path) {
1361				if (array_key_exists('lld_macro_pathid', $lld_macro_path)) {
1362					// Update records by primary key.
1363
1364					// Find matching "lld_macro_pathid" and update fields accordingly.
1365					if (bccomp($lld_macro_path['lld_macro_pathid'], $db_lld_macro_path['lld_macro_pathid']) == 0) {
1366						$fields_to_update = [];
1367
1368						if (array_key_exists('lld_macro', $lld_macro_path)
1369								&& $lld_macro_path['lld_macro'] === $db_lld_macro_path['lld_macro']) {
1370							// If same "lld_macro" is found in DB, update only "path" if necessary.
1371
1372							if (array_key_exists('path', $lld_macro_path)
1373									&& $lld_macro_path['path'] !== $lld_macro_path['path']) {
1374								$fields_to_update['path'] = $lld_macro_path['path'];
1375							}
1376						}
1377						else {
1378							/*
1379							 * Update all other fields that correspond to given "lld_macro_pathid". Except for primary
1380							 * key "lld_macro_pathid" and "itemid".
1381							 */
1382
1383							foreach ($lld_macro_path as $field => $value) {
1384								if ($field !== 'itemid' && $field !== 'lld_macro_pathid') {
1385									$fields_to_update[$field] = $value;
1386								}
1387							}
1388						}
1389
1390						/*
1391						 * If there are any changes made, update fields in DB. Otherwise skip updating and result in
1392						 * success anyway.
1393						 */
1394						if ($fields_to_update) {
1395							$lld_macro_paths_to_update[] = $fields_to_update
1396								+ ['lld_macro_pathid' => $lld_macro_path['lld_macro_pathid']];
1397						}
1398
1399						/*
1400						 * Remove processed LLD macros from the list. Macros left in $db_lld_macro_paths will be removed
1401						 * afterwards.
1402						 */
1403						unset($db_lld_macro_paths[$idx2]);
1404						unset($lld_macro_paths[$idx1]);
1405					}
1406					// Incorrect "lld_macro_pathid" cannot be given due to validation done previously.
1407				}
1408				else {
1409					// Add or update fields by given "lld_macro".
1410
1411					if (bccomp($lld_macro_path['itemid'], $db_lld_macro_path['itemid']) == 0) {
1412						if ($lld_macro_path['lld_macro'] === $db_lld_macro_path['lld_macro']) {
1413							// If same "lld_macro" is given, add primary key and update only "path", if necessary.
1414
1415							if ($lld_macro_path['path'] !== $db_lld_macro_path['path']) {
1416								$lld_macro_paths_to_update[] = [
1417									'lld_macro_pathid' => $db_lld_macro_path['lld_macro_pathid'],
1418									'path' => $lld_macro_path['path']
1419								];
1420							}
1421
1422							/*
1423							 * Remove processed LLD macros from the list. Macros left in $db_lld_macro_paths will
1424							 * be removed afterwards. And macros left in $lld_macro_paths will be created.
1425							 */
1426							unset($db_lld_macro_paths[$idx2]);
1427							unset($lld_macro_paths[$idx1]);
1428						}
1429					}
1430				}
1431			}
1432		}
1433
1434		// After all data has been collected, proceed with record update in DB.
1435		$lld_macro_pathids_to_delete = zbx_objectValues($db_lld_macro_paths, 'lld_macro_pathid');
1436
1437		if ($lld_macro_pathids_to_delete) {
1438			DB::delete('lld_macro_path', ['lld_macro_pathid' => $lld_macro_pathids_to_delete]);
1439		}
1440
1441		if ($lld_macro_paths_to_update) {
1442			$data = [];
1443
1444			foreach ($lld_macro_paths_to_update as $lld_macro_path) {
1445				$data[] = [
1446					'values' => $lld_macro_path,
1447					'where' => [
1448						'lld_macro_pathid' => $lld_macro_path['lld_macro_pathid']
1449					]
1450				];
1451			}
1452
1453			DB::update('lld_macro_path', $data);
1454		}
1455
1456		DB::insertBatch('lld_macro_path', $lld_macro_paths);
1457
1458		$this->updateItemParameters($items);
1459		$this->updateItemPreprocessing($items);
1460
1461		// Delete old overrides and replace with new ones if any.
1462		$ovrd_itemids = [];
1463		foreach ($items as $item) {
1464			if (array_key_exists('overrides', $item)) {
1465				$ovrd_itemids[$item['itemid']] = true;
1466			}
1467		}
1468
1469		if ($ovrd_itemids) {
1470			DBexecute('DELETE FROM lld_override WHERE '.dbConditionId('itemid', array_keys($ovrd_itemids)));
1471		}
1472
1473		$this->createOverrides($items);
1474	}
1475
1476	/**
1477	 * Converts a formula with letters to a formula with IDs and updates it.
1478	 *
1479	 * @param string 	$itemId
1480	 * @param string 	$evalFormula		formula with letters
1481	 * @param array 	$conditions
1482	 */
1483	protected function updateFormula($itemId, $evalFormula, array $conditions) {
1484		$ids = [];
1485		foreach ($conditions as $condition) {
1486			$ids[$condition['formulaid']] = $condition['item_conditionid'];
1487		}
1488		$formula = CConditionHelper::replaceLetterIds($evalFormula, $ids);
1489
1490		DB::updateByPk('items', $itemId, [
1491			'formula' => $formula
1492		]);
1493	}
1494
1495	/**
1496	 * Check item data and set missing default values.
1497	 *
1498	 * @param array $items passed by reference
1499	 * @param bool  $update
1500	 * @param array $dbItems
1501	 */
1502	protected function checkInput(array &$items, $update = false, array $dbItems = []) {
1503		// add the values that cannot be changed, but are required for further processing
1504		foreach ($items as &$item) {
1505			$item['flags'] = ZBX_FLAG_DISCOVERY_RULE;
1506			$item['value_type'] = ITEM_VALUE_TYPE_TEXT;
1507
1508			// unset fields that are updated using the 'filter' parameter
1509			unset($item['evaltype']);
1510			unset($item['formula']);
1511		}
1512		unset($item);
1513
1514		parent::checkInput($items, $update);
1515
1516		$validateItems = $items;
1517		if ($update) {
1518			$validateItems = $this->extendFromObjects(zbx_toHash($validateItems, 'itemid'), $dbItems, ['name']);
1519		}
1520
1521		// filter validator
1522		$filterValidator = new CSchemaValidator($this->getFilterSchema());
1523
1524		// condition validation
1525		$conditionValidator = new CSchemaValidator($this->getFilterConditionSchema());
1526		foreach ($validateItems as $item) {
1527			// validate custom formula and conditions
1528			if (isset($item['filter'])) {
1529				$filterValidator->setObjectName($item['name']);
1530				$this->checkValidator($item['filter'], $filterValidator);
1531
1532				foreach ($item['filter']['conditions'] as $condition) {
1533					$conditionValidator->setObjectName($item['name']);
1534					$this->checkValidator($condition, $conditionValidator);
1535				}
1536			}
1537		}
1538
1539		$this->validateOverrides($validateItems);
1540	}
1541
1542	/**
1543	 * Validate low-level discovery rule overrides.
1544	 *
1545	 * @param array $items  Low-level discovery rules.
1546	 *
1547	 * @throws APIException
1548	 */
1549	protected function validateOverrides(array $items): void {
1550		$api_input_rules = ['type' => API_OBJECTS, 'uniq' => [['name'], ['step']], 'fields' => [
1551			'step' =>			['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => '1:'.ZBX_MAX_INT32],
1552			'name' =>			['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('lld_override', 'name')],
1553			'stop' =>			['type' => API_INT32, 'in' => implode(',', [ZBX_LLD_OVERRIDE_STOP_NO, ZBX_LLD_OVERRIDE_STOP_YES]), 'default' => ZBX_LLD_OVERRIDE_STOP_NO],
1554			'filter' =>			['type' => API_OBJECT, 'fields' => [
1555				'evaltype' =>		['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [CONDITION_EVAL_TYPE_AND_OR, CONDITION_EVAL_TYPE_AND, CONDITION_EVAL_TYPE_OR, CONDITION_EVAL_TYPE_EXPRESSION])],
1556				'formula' =>		['type' => API_STRING_UTF8],
1557				'conditions' =>		['type' => API_OBJECTS, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'fields' => [
1558					'macro' =>			['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('lld_override_condition', 'macro')],
1559					'operator' =>		['type' => API_INT32, 'in' => implode(',', [CONDITION_OPERATOR_REGEXP, CONDITION_OPERATOR_NOT_REGEXP, CONDITION_OPERATOR_EXISTS, CONDITION_OPERATOR_NOT_EXISTS]), 'default' => DB::getDefault('lld_override_condition', 'operator')],
1560					'value' =>			['type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('lld_override_condition', 'value')],
1561					'formulaid' =>		['type' => API_STRING_UTF8]
1562				]]
1563			]],
1564			'operations' =>	['type' => API_OBJECTS, 'fields' => [
1565				'operationobject' =>	['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [OPERATION_OBJECT_ITEM_PROTOTYPE, OPERATION_OBJECT_TRIGGER_PROTOTYPE, OPERATION_OBJECT_GRAPH_PROTOTYPE, OPERATION_OBJECT_HOST_PROTOTYPE])],
1566				'operator' =>			['type' => API_INT32, 'in' => implode(',', [CONDITION_OPERATOR_EQUAL, CONDITION_OPERATOR_NOT_EQUAL, CONDITION_OPERATOR_LIKE, CONDITION_OPERATOR_NOT_LIKE, CONDITION_OPERATOR_REGEXP, CONDITION_OPERATOR_NOT_REGEXP]), 'default' => DB::getDefault('lld_override_operation', 'operator')],
1567				'value' =>				['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('lld_override_operation', 'value')],
1568				'opstatus' =>			['type' => API_OBJECT, 'fields' => [
1569					'status' =>				['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [ZBX_PROTOTYPE_STATUS_ENABLED, ZBX_PROTOTYPE_STATUS_DISABLED])]
1570				]],
1571				'opdiscover' =>			['type' => API_OBJECT, 'fields' => [
1572					'discover' =>			['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [ZBX_PROTOTYPE_DISCOVER, ZBX_PROTOTYPE_NO_DISCOVER])]
1573				]],
1574				'opperiod' =>			['type' => API_OBJECT, 'fields' => [
1575					'delay' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('lld_override_opperiod', 'delay')]
1576				]],
1577				'ophistory' =>			['type' => API_OBJECT, 'fields' => [
1578					'history' =>			['type' => API_TIME_UNIT, 'flags' => API_REQUIRED | API_NOT_EMPTY | API_ALLOW_USER_MACRO | API_ALLOW_LLD_MACRO, 'in' => '0,'.implode(':', [SEC_PER_HOUR, 25 * SEC_PER_YEAR]), 'length' => DB::getFieldLength('lld_override_ophistory', 'history')]
1579				]],
1580				'optrends' =>			['type' => API_OBJECT, 'fields' => [
1581					'trends' =>				['type' => API_TIME_UNIT, 'flags' => API_REQUIRED | API_NOT_EMPTY | API_ALLOW_USER_MACRO | API_ALLOW_LLD_MACRO, 'in' => '0,'.implode(':', [SEC_PER_HOUR, 25 * SEC_PER_YEAR]), 'length' => DB::getFieldLength('lld_override_optrends', 'trends')]
1582				]],
1583				'opseverity' =>			['type' => API_OBJECT, 'fields' => [
1584					'severity' =>			['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', range(TRIGGER_SEVERITY_NOT_CLASSIFIED, TRIGGER_SEVERITY_COUNT - 1))]
1585				]],
1586				'optag' =>				['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY, 'uniq' => [['tag', 'value']], 'fields' => [
1587					'tag' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('lld_override_optag', 'tag')],
1588					'value' =>				['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('lld_override_optag', 'value'), 'default' => DB::getDefault('lld_override_optag', 'value')]
1589				]],
1590				'optemplate' =>			['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY, 'fields' => [
1591					'templateid' =>			['type' => API_ID, 'flags' => API_REQUIRED]
1592				]],
1593				'opinventory' =>		['type' => API_OBJECT, 'fields' => [
1594					'inventory_mode' =>		['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [HOST_INVENTORY_DISABLED, HOST_INVENTORY_MANUAL, HOST_INVENTORY_AUTOMATIC])]
1595				]]
1596			]]
1597		]];
1598
1599		// Schema for filter is already validated in API validator. Create the formula validator for filter.
1600		$condition_validator = new CConditionValidator([
1601			'messageMissingFormula' => _('Formula missing for override "%1$s".'),
1602			'messageInvalidFormula' => _('Incorrect custom expression "%2$s" for override "%1$s": %3$s.'),
1603			'messageMissingCondition' =>
1604				_('Condition "%2$s" used in formula "%3$s" for override "%1$s" is not defined.'),
1605			'messageUnusedCondition' => _('Condition "%2$s" is not used in formula "%3$s" for override "%1$s".')
1606		]);
1607
1608		$update_interval_parser = new CUpdateIntervalParser([
1609			'usermacros' => true,
1610			'lldmacros' => true
1611		]);
1612
1613		$lld_idx = 0;
1614		foreach ($items as $item) {
1615			if (array_key_exists('overrides', $item)) {
1616				$path = '/'.(++$lld_idx).'/overrides';
1617
1618				if (!CApiInputValidator::validate($api_input_rules, $item['overrides'], $path, $error)) {
1619					self::exception(ZBX_API_ERROR_PARAMETERS, $error);
1620				}
1621
1622				foreach ($item['overrides'] as $ovrd_idx => $override) {
1623					if (array_key_exists('filter', $override)) {
1624						$condition_validator->setObjectName($override['name']);
1625
1626						// Validate the formula and check if they are in the conditions.
1627						if (!$condition_validator->validate($override['filter'])) {
1628							self::exception(ZBX_API_ERROR_PARAMETERS, $condition_validator->getError());
1629						}
1630
1631						// Validate that conditions have correct macros and 'formulaid' for custom expressions.
1632						if (array_key_exists('conditions', $override['filter'])) {
1633							foreach ($override['filter']['conditions'] as $cnd_idx => $condition) {
1634								// API validator only checks if 'macro' field exists and is not empty. It must be macro.
1635								if (!preg_match('/^'.ZBX_PREG_EXPRESSION_LLD_MACROS.'$/', $condition['macro'])) {
1636									self::exception(ZBX_API_ERROR_PARAMETERS,
1637										_s('Incorrect filter condition macro for override "%1$s".', $override['name'])
1638									);
1639								}
1640
1641								if ($override['filter']['evaltype'] == CONDITION_EVAL_TYPE_EXPRESSION) {
1642									/*
1643									 * Check only if 'formulaid' exists. It cannot be empty or incorrect, but that is
1644									 * already validated by previously set conditionValidator.
1645									 */
1646									if (!array_key_exists('formulaid', $condition)) {
1647										$cond_path = $path.'/'.($ovrd_idx + 1).'/filter/conditions/'.($cnd_idx + 1);
1648										self::exception(ZBX_API_ERROR_PARAMETERS,
1649											_s('Invalid parameter "%1$s": %2$s.', $path,
1650												_s('the parameter "%1$s" is missing', 'formulaid')
1651											)
1652										);
1653									}
1654								}
1655							}
1656						}
1657					}
1658
1659					// Check integrity of 'overrideobject' and its fields.
1660					if (array_key_exists('operations', $override)) {
1661						foreach ($override['operations'] as $opr_idx => $operation) {
1662							$opr_path = $path.'/'.($ovrd_idx + 1).'/operations/'.($opr_idx + 1);
1663
1664							switch ($operation['operationobject']) {
1665								case OPERATION_OBJECT_ITEM_PROTOTYPE:
1666									foreach (['opseverity', 'optemplate', 'opinventory'] as $field) {
1667										if (array_key_exists($field, $operation)) {
1668											self::exception(ZBX_API_ERROR_PARAMETERS,
1669												_s('Invalid parameter "%1$s": %2$s.', $opr_path,
1670													_s('unexpected parameter "%1$s"', $field)
1671												)
1672											);
1673										}
1674									}
1675
1676									if (!array_key_exists('opstatus', $operation)
1677											&& !array_key_exists('opperiod', $operation)
1678											&& !array_key_exists('ophistory', $operation)
1679											&& !array_key_exists('optrends', $operation)
1680											&& !array_key_exists('optag', $operation)
1681											&& !array_key_exists('opdiscover', $operation)) {
1682										self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.',
1683											$opr_path, _s('value must be one of %1$s',
1684												'opstatus, opdiscover, opperiod, ophistory, optrends, optag'
1685											)
1686										));
1687									}
1688
1689									if (array_key_exists('opperiod', $operation)
1690											&& !validateDelay($update_interval_parser, 'delay',
1691												$operation['opperiod']['delay'], $error)) {
1692										self::exception(ZBX_API_ERROR_PARAMETERS, $error);
1693									}
1694									break;
1695
1696								case OPERATION_OBJECT_TRIGGER_PROTOTYPE:
1697									foreach (['opperiod', 'ophistory', 'optrends', 'optemplate', 'opinventory'] as
1698											$field) {
1699										if (array_key_exists($field, $operation)) {
1700											self::exception(ZBX_API_ERROR_PARAMETERS,
1701												_s('Invalid parameter "%1$s": %2$s.', $opr_path,
1702													_s('unexpected parameter "%1$s"', $field)
1703												)
1704											);
1705										}
1706									}
1707
1708									if (!array_key_exists('opstatus', $operation)
1709											&& !array_key_exists('opseverity', $operation)
1710											&& !array_key_exists('optag', $operation)
1711											&& !array_key_exists('opdiscover', $operation)) {
1712										self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.',
1713											$opr_path,
1714											_s('value must be one of %1$s', 'opstatus, opdiscover, opseverity, optag')
1715										));
1716									}
1717									break;
1718
1719								case OPERATION_OBJECT_GRAPH_PROTOTYPE:
1720									foreach (['opstatus', 'opperiod', 'ophistory', 'optrends', 'opseverity', 'optag',
1721											'optemplate', 'opinventory'] as $field) {
1722										if (array_key_exists($field, $operation)) {
1723											self::exception(ZBX_API_ERROR_PARAMETERS,
1724												_s('Invalid parameter "%1$s": %2$s.', $opr_path,
1725													_s('unexpected parameter "%1$s"', $field)
1726												)
1727											);
1728										}
1729									}
1730
1731									if (!array_key_exists('opdiscover', $operation)) {
1732										self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.',
1733											$opr_path,
1734											_s('value must be one of %1$s', 'opdiscover')
1735										));
1736									}
1737									break;
1738
1739								case OPERATION_OBJECT_HOST_PROTOTYPE:
1740									foreach (['opperiod', 'ophistory', 'optrends', 'opseverity'] as $field) {
1741										if (array_key_exists($field, $operation)) {
1742											self::exception(ZBX_API_ERROR_PARAMETERS,
1743												_s('Invalid parameter "%1$s": %2$s.', $opr_path,
1744													_s('unexpected parameter "%1$s"', $field)
1745												)
1746											);
1747										}
1748									}
1749
1750									if (!array_key_exists('opstatus', $operation)
1751											&& !array_key_exists('optemplate', $operation)
1752											&& !array_key_exists('optag', $operation)
1753											&& !array_key_exists('opinventory', $operation)
1754											&& !array_key_exists('opdiscover', $operation)) {
1755										self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.',
1756											$opr_path, _s('value must be one of %1$s',
1757												'opstatus, opdiscover, optemplate, optag, opinventory'
1758											)
1759										));
1760									}
1761
1762									if (array_key_exists('optemplate', $operation)) {
1763										$templates_cnt = API::Template()->get([
1764											'countOutput' => true,
1765											'templateids' => zbx_objectValues($operation['optemplate'], 'templateid')
1766										]);
1767
1768										if (count($operation['optemplate']) != $templates_cnt) {
1769											self::exception(ZBX_API_ERROR_PERMISSIONS,
1770												_('No permissions to referred object or it does not exist!')
1771											);
1772										}
1773									}
1774									break;
1775							}
1776						}
1777					}
1778				}
1779			}
1780		}
1781	}
1782
1783	/**
1784	 * Returns the parameters for creating a discovery rule filter validator.
1785	 *
1786	 * @return array
1787	 */
1788	protected function getFilterSchema() {
1789		return [
1790			'validators' => [
1791				'evaltype' => new CLimitedSetValidator([
1792					'values' => [
1793						CONDITION_EVAL_TYPE_OR,
1794						CONDITION_EVAL_TYPE_AND,
1795						CONDITION_EVAL_TYPE_AND_OR,
1796						CONDITION_EVAL_TYPE_EXPRESSION
1797					],
1798					'messageInvalid' => _('Incorrect type of calculation for discovery rule "%1$s".')
1799				]),
1800				'formula' => new CStringValidator([
1801					'empty' => true
1802				]),
1803				'conditions' => new CCollectionValidator([
1804					'empty' => true,
1805					'messageInvalid' => _('Incorrect conditions for discovery rule "%1$s".')
1806				])
1807			],
1808			'postValidators' => [
1809				new CConditionValidator([
1810					'messageMissingFormula' => _('Formula missing for discovery rule "%1$s".'),
1811					'messageInvalidFormula' => _('Incorrect custom expression "%2$s" for discovery rule "%1$s": %3$s.'),
1812					'messageMissingCondition' => _('Condition "%2$s" used in formula "%3$s" for discovery rule "%1$s" is not defined.'),
1813					'messageUnusedCondition' => _('Condition "%2$s" is not used in formula "%3$s" for discovery rule "%1$s".')
1814				])
1815			],
1816			'required' => ['evaltype', 'conditions'],
1817			'messageRequired' => _('No "%2$s" given for the filter of discovery rule "%1$s".'),
1818			'messageUnsupported' => _('Unsupported parameter "%2$s" for the filter of discovery rule "%1$s".')
1819		];
1820	}
1821
1822	/**
1823	 * Returns the parameters for creating a discovery rule filter condition validator.
1824	 *
1825	 * @return array
1826	 */
1827	protected function getFilterConditionSchema() {
1828		return [
1829			'validators' => [
1830				'macro' => new CStringValidator([
1831					'regex' => '/^'.ZBX_PREG_EXPRESSION_LLD_MACROS.'$/',
1832					'messageEmpty' => _('Empty filter condition macro for discovery rule "%1$s".'),
1833					'messageRegex' => _('Incorrect filter condition macro for discovery rule "%1$s".')
1834				]),
1835				'value' => new CStringValidator([
1836					'empty' => true
1837				]),
1838				'formulaid' => new CStringValidator([
1839					'regex' => '/[A-Z]+/',
1840					'messageEmpty' => _('Empty filter condition formula ID for discovery rule "%1$s".'),
1841					'messageRegex' => _('Incorrect filter condition formula ID for discovery rule "%1$s".')
1842				]),
1843				'operator' => new CLimitedSetValidator([
1844					'values' => [CONDITION_OPERATOR_REGEXP, CONDITION_OPERATOR_NOT_REGEXP, CONDITION_OPERATOR_EXISTS,
1845						CONDITION_OPERATOR_NOT_EXISTS
1846					],
1847					'messageInvalid' => _('Incorrect filter condition operator for discovery rule "%1$s".')
1848				])
1849			],
1850			'required' => ['macro', 'value'],
1851			'messageRequired' => _('No "%2$s" given for a filter condition of discovery rule "%1$s".'),
1852			'messageUnsupported' => _('Unsupported parameter "%2$s" for a filter condition of discovery rule "%1$s".')
1853		];
1854	}
1855
1856	/**
1857	 * Check discovery rule specific fields.
1858	 *
1859	 * @param array  $item    An array of single item data.
1860	 * @param string $method  A string of "create" or "update" method.
1861	 *
1862	 * @throws APIException if the input is invalid.
1863	 */
1864	protected function checkSpecificFields(array $item, $method) {
1865		if (array_key_exists('lifetime', $item)
1866				&& !validateTimeUnit($item['lifetime'], SEC_PER_HOUR, 25 * SEC_PER_YEAR, true, $error,
1867					['usermacros' => true])) {
1868			self::exception(ZBX_API_ERROR_PARAMETERS,
1869				_s('Incorrect value for field "%1$s": %2$s.', 'lifetime', $error)
1870			);
1871		}
1872	}
1873
1874	/**
1875	 * Checks if LLD macros contain duplicate names in "lld_macro".
1876	 *
1877	 * @param array  $lld_macro_paths                 Array of items to validate.
1878	 * @param string $lld_macro_paths[]['lld_macro']  LLD macro string (optional for update method).
1879	 * @param array  $macro_names                     Array where existing macro names are collected.
1880	 * @param string $path                            Path to API object.
1881	 *
1882	 * @throws APIException if same discovery rules contains duplicate LLD macro names.
1883	 */
1884	protected function checkDuplicateLLDMacros(array $lld_macro_paths, $macro_names, $path) {
1885		foreach ($lld_macro_paths as $num => $lld_macro_path) {
1886			if (array_key_exists('lld_macro', $lld_macro_path)) {
1887				if (array_key_exists($lld_macro_path['lld_macro'], $macro_names)) {
1888					self::exception(ZBX_API_ERROR_PARAMETERS,
1889						_s('Invalid parameter "%1$s": %2$s.', $path.'/lld_macro_paths/'.($num + 1).'/lld_macro',
1890							_s('value "%1$s" already exists', $lld_macro_path['lld_macro'])
1891						)
1892					);
1893				}
1894
1895				$macro_names[$lld_macro_path['lld_macro']] = true;
1896			}
1897		}
1898	}
1899
1900	/**
1901	 * Validates parameters in "lld_macro_paths" property for each item in create method.
1902	 *
1903	 * @param array  $items                                      Array of items to validate.
1904	 * @param array  $items[]['lld_macro_paths']                 Array of LLD macro paths to validate for each
1905	 *                                                           discovery rule (optional).
1906	 * @param string $items[]['lld_macro_paths'][]['lld_macro']  LLD macro string. Required if "lld_macro_paths" exists.
1907	 * @param string $items[]['lld_macro_paths'][]['path']       Path string. Validates as regular string. Required if
1908	 *                                                           "lld_macro_paths" exists.
1909	 *
1910	 * @throws APIException if incorrect fields and values given.
1911	 */
1912	protected function validateCreateLLDMacroPaths(array $items) {
1913		$rules = [
1914			'lld_macro_paths' =>	['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY, 'fields' => [
1915				'lld_macro' =>			['type' => API_LLD_MACRO, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('lld_macro_path', 'lld_macro')],
1916				'path' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('lld_macro_path', 'path')]
1917			]]
1918		];
1919
1920		foreach ($items as $key => $item) {
1921			if (array_key_exists('lld_macro_paths', $item)) {
1922				$item = array_intersect_key($item, $rules);
1923				$path = '/'.($key + 1);
1924
1925				if (!CApiInputValidator::validate(['type' => API_OBJECT, 'fields' => $rules], $item, $path, $error)) {
1926					self::exception(ZBX_API_ERROR_PARAMETERS, $error);
1927				}
1928
1929				$this->checkDuplicateLLDMacros($item['lld_macro_paths'], [], $path);
1930			}
1931		}
1932	}
1933
1934	/**
1935	 * Validates parameters in "lld_macro_paths" property for each item in create method.
1936	 *
1937	 * @param array  $items                                             Array of items to validate.
1938	 * @param array  $items[]['lld_macro_paths']                        Array of LLD macro paths to validate for each
1939	 *                                                                  discovery rule (optional).
1940	 * @param string $items[]['lld_macro_paths'][]['lld_macro_pathid']  LLD macro path ID from DB (optional).
1941	 * @param string $items[]['lld_macro_paths'][]['lld_macro']         LLD macro string. Required if "lld_macro_pathid"
1942	 *                                                                  does not exist.
1943	 * @param string $items[]['lld_macro_paths'][]['path']              Path string. Validates as regular string.
1944	 *                                                                  Required if "lld_macro_pathid" and "lld_macro"
1945	 *                                                                  do not exist.
1946	 *
1947	 * @throws APIException if incorrect fields and values given.
1948	 */
1949	protected function validateUpdateLLDMacroPaths(array $items) {
1950		$rules = [
1951			'lld_macro_paths' =>	['type' => API_OBJECTS, 'fields' => [
1952				'lld_macro_pathid' =>	['type' => API_ID],
1953				'lld_macro' =>			['type' => API_LLD_MACRO, 'length' => DB::getFieldLength('lld_macro_path', 'lld_macro')],
1954				'path' =>				['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('lld_macro_path', 'path')]
1955			]]
1956		];
1957
1958		$items = $this->extendObjects('items', $items, ['templateid']);
1959
1960		foreach ($items as $key => $item) {
1961			if (array_key_exists('lld_macro_paths', $item)) {
1962				$itemid = $item['itemid'];
1963				$templateid = $item['templateid'];
1964
1965				$item = array_intersect_key($item, $rules);
1966				$path = '/'.($key + 1);
1967
1968				if (!CApiInputValidator::validate(['type' => API_OBJECT, 'fields' => $rules], $item, $path, $error)) {
1969					self::exception(ZBX_API_ERROR_PARAMETERS, $error);
1970				}
1971
1972				if (array_key_exists('lld_macro_paths', $item)) {
1973					if ($templateid != 0) {
1974						self::exception(ZBX_API_ERROR_PARAMETERS,
1975							_s('Invalid parameter "%1$s": %2$s.', $path.'/lld_macro_paths',
1976								_('cannot update property for templated discovery rule')
1977							)
1978						);
1979					}
1980
1981					$lld_macro_pathids = [];
1982
1983					// Check that fields exists, are not empty, do not duplicate and collect IDs to compare with DB.
1984					foreach ($item['lld_macro_paths'] as $num => $lld_macro_path) {
1985						$subpath = $num + 1;
1986
1987						// API_NOT_EMPTY will not work, so we need at least one field to be present.
1988						if (!array_key_exists('lld_macro', $lld_macro_path)
1989								&& !array_key_exists('path', $lld_macro_path)
1990								&& !array_key_exists('lld_macro_pathid', $lld_macro_path)) {
1991							self::exception(ZBX_API_ERROR_PARAMETERS,
1992								_s('Invalid parameter "%1$s": %2$s.', $path.'/lld_macro_paths/'.$subpath,
1993									_('cannot be empty')
1994								)
1995							);
1996						}
1997
1998						// API 'uniq' => true will not work, because we validate API_ID not API_IDS. So make IDs unique.
1999						if (array_key_exists('lld_macro_pathid', $lld_macro_path)) {
2000							$lld_macro_pathids[$lld_macro_path['lld_macro_pathid']] = true;
2001						}
2002						else {
2003							/*
2004							 * In case "lld_macro_pathid" does not exist, we need to treat it as a new LLD macro with
2005							 * both fields present.
2006							 */
2007							if (array_key_exists('lld_macro', $lld_macro_path)
2008									&& !array_key_exists('path', $lld_macro_path)) {
2009								self::exception(ZBX_API_ERROR_PARAMETERS,
2010									_s('Invalid parameter "%1$s": %2$s.', $path.'/lld_macro_paths/'.$subpath,
2011										_s('the parameter "%1$s" is missing', 'path')
2012									)
2013								);
2014							}
2015							elseif (array_key_exists('path', $lld_macro_path)
2016									&& !array_key_exists('lld_macro', $lld_macro_path)) {
2017								self::exception(ZBX_API_ERROR_PARAMETERS,
2018									_s('Invalid parameter "%1$s": %2$s.', $path.'/lld_macro_paths/'.$subpath,
2019										_s('the parameter "%1$s" is missing', 'lld_macro')
2020									)
2021								);
2022							}
2023						}
2024					}
2025
2026					$this->checkDuplicateLLDMacros($item['lld_macro_paths'], [], $path);
2027
2028					/*
2029					 * Validate "lld_macro_pathid" field. If "lld_macro_pathid" doesn't correspond to given "itemid"
2030					 * or does not exist, throw an exception.
2031					 */
2032					if ($lld_macro_pathids) {
2033						$lld_macro_pathids = array_keys($lld_macro_pathids);
2034
2035						$db_lld_macro_paths = DBfetchArrayAssoc(DBselect(
2036							'SELECT lmp.lld_macro_pathid,lmp.lld_macro'.
2037							' FROM lld_macro_path lmp'.
2038							' WHERE lmp.itemid='.zbx_dbstr($itemid).
2039								' AND '.dbConditionId('lmp.lld_macro_pathid', $lld_macro_pathids)
2040						), 'lld_macro_pathid');
2041
2042						if (count($db_lld_macro_paths) != count($lld_macro_pathids)) {
2043							self::exception(ZBX_API_ERROR_PERMISSIONS,
2044								_('No permissions to referred object or it does not exist!')
2045							);
2046						}
2047
2048						$macro_names = [];
2049
2050						foreach ($item['lld_macro_paths'] as $num => $lld_macro_path) {
2051							if (array_key_exists('lld_macro_pathid', $lld_macro_path)
2052									&& !array_key_exists('lld_macro', $lld_macro_path)) {
2053								$db_lld_macro_path = $db_lld_macro_paths[$lld_macro_path['lld_macro_pathid']];
2054								$macro_names[$db_lld_macro_path['lld_macro']] = true;
2055							}
2056						}
2057
2058						$this->checkDuplicateLLDMacros($item['lld_macro_paths'], $macro_names, $path);
2059					}
2060				}
2061			}
2062		}
2063	}
2064
2065	/**
2066	 * Copies the given discovery rule to the specified host.
2067	 *
2068	 * @throws APIException if the discovery rule interfaces could not be mapped
2069	 * to the new host interfaces.
2070	 *
2071	 * @param string $discoveryid  The ID of the discovery rule to be copied
2072	 * @param string $hostid       Destination host id
2073	 *
2074	 * @return bool
2075	 */
2076	protected function copyDiscoveryRule($discoveryid, $hostid) {
2077		// fetch discovery to clone
2078		$srcDiscovery = $this->get([
2079			'itemids' => $discoveryid,
2080			'output' => ['itemid', 'type', 'snmp_oid', 'hostid', 'name', 'key_', 'delay', 'history', 'trends', 'status',
2081				'value_type', 'trapper_hosts', 'units', 'lastlogsize', 'logtimefmt', 'valuemapid', 'params',
2082				'ipmi_sensor', 'authtype', 'username', 'password', 'publickey', 'privatekey', 'mtime', 'flags',
2083				'interfaceid', 'description', 'inventory_link', 'lifetime', 'jmx_endpoint', 'url', 'query_fields',
2084				'parameters', 'timeout', 'posts', 'status_codes', 'follow_redirects', 'post_type', 'http_proxy',
2085				'headers', 'retrieve_mode', 'request_method', 'ssl_cert_file', 'ssl_key_file', 'ssl_key_password',
2086				'verify_peer', 'verify_host', 'allow_traps', 'master_itemid'
2087			],
2088			'selectFilter' => ['evaltype', 'formula', 'conditions'],
2089			'selectLLDMacroPaths' => ['lld_macro', 'path'],
2090			'selectPreprocessing' => ['type', 'params', 'error_handler', 'error_handler_params'],
2091			'selectOverrides' => ['name', 'step', 'stop', 'filter', 'operations'],
2092			'preservekeys' => true
2093		]);
2094		$srcDiscovery = reset($srcDiscovery);
2095
2096		// fetch source and destination hosts
2097		$hosts = API::Host()->get([
2098			'hostids' => [$srcDiscovery['hostid'], $hostid],
2099			'output' => ['hostid', 'host', 'name', 'status'],
2100			'selectInterfaces' => API_OUTPUT_EXTEND,
2101			'templated_hosts' => true,
2102			'preservekeys' => true
2103		]);
2104		$srcHost = $hosts[$srcDiscovery['hostid']];
2105		$dstHost = $hosts[$hostid];
2106
2107		$dstDiscovery = $srcDiscovery;
2108		$dstDiscovery['hostid'] = $hostid;
2109		unset($dstDiscovery['itemid']);
2110		if ($dstDiscovery['filter']) {
2111			foreach ($dstDiscovery['filter']['conditions'] as &$condition) {
2112				unset($condition['itemid'], $condition['item_conditionid']);
2113			}
2114			unset($condition);
2115		}
2116
2117		if (!$dstDiscovery['lld_macro_paths']) {
2118			unset($dstDiscovery['lld_macro_paths']);
2119		}
2120
2121		if ($dstDiscovery['overrides']) {
2122			foreach ($dstDiscovery['overrides'] as &$override) {
2123				if (array_key_exists('filter', $override)) {
2124					if (!$override['filter']['conditions']) {
2125						unset($override['filter']);
2126					}
2127					unset($override['filter']['eval_formula']);
2128				}
2129			}
2130			unset($override);
2131		}
2132		else {
2133			unset($dstDiscovery['overrides']);
2134		}
2135
2136		// if this is a plain host, map discovery interfaces
2137		if ($srcHost['status'] != HOST_STATUS_TEMPLATE) {
2138			// find a matching interface
2139			$interface = self::findInterfaceForItem($dstDiscovery['type'], $dstHost['interfaces']);
2140			if ($interface) {
2141				$dstDiscovery['interfaceid'] = $interface['interfaceid'];
2142			}
2143			// no matching interface found, throw an error
2144			elseif ($interface !== false) {
2145				self::exception(ZBX_API_ERROR_PARAMETERS, _s(
2146					'Cannot find host interface on "%1$s" for item key "%2$s".',
2147					$dstHost['name'],
2148					$dstDiscovery['key_']
2149				));
2150			}
2151		}
2152
2153		// Master item should exists for LLD rule with type dependent item.
2154		if ($srcDiscovery['type'] == ITEM_TYPE_DEPENDENT) {
2155			$master_items = DBfetchArray(DBselect(
2156				'SELECT i1.itemid'.
2157				' FROM items i1,items i2'.
2158				' WHERE i1.key_=i2.key_'.
2159					' AND i1.hostid='.zbx_dbstr($dstDiscovery['hostid']).
2160					' AND i2.itemid='.zbx_dbstr($srcDiscovery['master_itemid'])
2161			));
2162
2163			if (!$master_items) {
2164				self::exception(ZBX_API_ERROR_PERMISSIONS,
2165					_s('Discovery rule "%1$s" cannot be copied without its master item.', $srcDiscovery['name'])
2166				);
2167			}
2168
2169			$dstDiscovery['master_itemid'] = $master_items[0]['itemid'];
2170		}
2171
2172		// save new discovery
2173		$newDiscovery = $this->create([$dstDiscovery]);
2174		$dstDiscovery['itemid'] = $newDiscovery['itemids'][0];
2175
2176		// copy prototypes
2177		$new_prototypeids = $this->copyItemPrototypes($srcDiscovery, $dstDiscovery, $dstHost);
2178
2179		// if there were prototypes defined, clone everything else
2180		if ($new_prototypeids) {
2181			// fetch new prototypes
2182			$dstDiscovery['items'] = API::ItemPrototype()->get([
2183				'output' => ['itemid', 'key_'],
2184				'itemids' => $new_prototypeids,
2185				'preservekeys' => true
2186			]);
2187
2188			// copy graphs
2189			$this->copyGraphPrototypes($srcDiscovery, $dstDiscovery);
2190
2191			// copy triggers
2192			$this->copyTriggerPrototypes($srcDiscovery, $srcHost, $dstHost);
2193		}
2194
2195		// copy host prototypes
2196		$this->copyHostPrototypes($discoveryid, $dstDiscovery);
2197
2198		return true;
2199	}
2200
2201	/**
2202	 * Copies all of the item prototypes from the source discovery to the target
2203	 * discovery rule. Return array of created item prototype ids.
2204	 *
2205	 * @throws APIException if prototype saving fails
2206	 *
2207	 * @param array $srcDiscovery   The source discovery rule to copy from
2208	 * @param array $dstDiscovery   The target discovery rule to copy to
2209	 * @param array $dstHost        The target host to copy the deiscovery rule to
2210	 *
2211	 * @return array
2212	 */
2213	protected function copyItemPrototypes(array $srcDiscovery, array $dstDiscovery, array $dstHost) {
2214		$item_prototypes = API::ItemPrototype()->get([
2215			'output' => ['itemid', 'type', 'snmp_oid', 'name', 'key_', 'delay', 'history', 'trends', 'status',
2216				'value_type', 'trapper_hosts', 'units', 'logtimefmt', 'valuemapid', 'params', 'ipmi_sensor', 'authtype',
2217				'username', 'password', 'publickey', 'privatekey', 'interfaceid', 'port', 'description', 'jmx_endpoint',
2218				'master_itemid', 'templateid', 'url', 'query_fields', 'timeout', 'posts', 'status_codes',
2219				'follow_redirects', 'post_type', 'http_proxy', 'headers', 'retrieve_mode', 'request_method',
2220				'output_format', 'ssl_cert_file', 'ssl_key_file', 'ssl_key_password', 'verify_peer', 'verify_host',
2221				'allow_traps', 'discover', 'parameters'
2222			],
2223			'selectPreprocessing' => ['type', 'params', 'error_handler', 'error_handler_params'],
2224			'selectTags' => ['tag', 'value'],
2225			'selectValueMap' => ['name'],
2226			'discoveryids' => $srcDiscovery['itemid'],
2227			'preservekeys' => true
2228		]);
2229		$new_itemids = [];
2230		$itemkey_to_id = [];
2231		$create_items = [];
2232		$src_valuemap_names = [];
2233		$valuemap_map = [];
2234
2235		foreach ($item_prototypes as $item_prototype) {
2236			if ($item_prototype['valuemap']) {
2237				$src_valuemap_names[] = $item_prototype['valuemap']['name'];
2238			}
2239		}
2240
2241		if ($src_valuemap_names) {
2242			$valuemap_map = array_column(API::ValueMap()->get([
2243				'output' => ['valuemapid', 'name'],
2244				'hostids' => $dstHost['hostid'],
2245				'filter' => ['name' => $src_valuemap_names]
2246			]), 'valuemapid', 'name');
2247		}
2248
2249		if ($item_prototypes) {
2250			$create_order = [];
2251			$src_itemid_to_key = [];
2252			$unresolved_master_itemids = [];
2253
2254			// Gather all master item IDs and check if master item IDs already belong to item prototypes.
2255			foreach ($item_prototypes as $itemid => $item_prototype) {
2256				if ($item_prototype['type'] == ITEM_TYPE_DEPENDENT
2257						&& !array_key_exists($item_prototype['master_itemid'], $item_prototypes)) {
2258					$unresolved_master_itemids[$item_prototype['master_itemid']] = true;
2259				}
2260			}
2261
2262			$items = [];
2263
2264			// It's possible that master items are non-prototype items.
2265			if ($unresolved_master_itemids) {
2266				$items = API::Item()->get([
2267					'output' => ['itemid'],
2268					'itemids' => array_keys($unresolved_master_itemids),
2269					'webitems' => true,
2270					'filter' => ['flags' => ZBX_FLAG_DISCOVERY_NORMAL],
2271					'preservekeys' => true
2272				]);
2273
2274				foreach ($items as $item) {
2275					if (array_key_exists($item['itemid'], $unresolved_master_itemids)) {
2276						unset($unresolved_master_itemids[$item['itemid']]);
2277					}
2278				}
2279
2280				// If still there are IDs left, there's nothing more we can do.
2281				if ($unresolved_master_itemids) {
2282					reset($unresolved_master_itemids);
2283					self::exception(ZBX_API_ERROR_PERMISSIONS, _s('Incorrect value for field "%1$s": %2$s.',
2284						'master_itemid', _s('Item "%1$s" does not exist or you have no access to this item',
2285							key($unresolved_master_itemids)
2286					)));
2287				}
2288			}
2289
2290			foreach ($item_prototypes as $itemid => $item_prototype) {
2291				$dependency_level = 0;
2292				$master_item_prototype = $item_prototype;
2293				$src_itemid_to_key[$itemid] = $item_prototype['key_'];
2294
2295				while ($master_item_prototype['type'] == ITEM_TYPE_DEPENDENT) {
2296					if (array_key_exists($master_item_prototype['master_itemid'], $item_prototypes)) {
2297						$master_item_prototype = $item_prototypes[$master_item_prototype['master_itemid']];
2298						++$dependency_level;
2299					}
2300					else {
2301						break;
2302					}
2303				}
2304
2305				$create_order[$itemid] = $dependency_level;
2306			}
2307			asort($create_order);
2308
2309			$current_dependency = reset($create_order);
2310
2311			foreach ($create_order as $key => $dependency_level) {
2312				if ($current_dependency != $dependency_level && $create_items) {
2313					$current_dependency = $dependency_level;
2314					$created_itemids = API::ItemPrototype()->create($create_items);
2315
2316					if (!$created_itemids) {
2317						self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot clone item prototypes.'));
2318					}
2319
2320					$created_itemids = $created_itemids['itemids'];
2321					$new_itemids = array_merge($new_itemids, $created_itemids);
2322
2323					foreach ($create_items as $index => $created_item) {
2324						$itemkey_to_id[$created_item['key_']] = $created_itemids[$index];
2325					}
2326
2327					$create_items = [];
2328				}
2329
2330				$item_prototype = $item_prototypes[$key];
2331				$item_prototype['ruleid'] = $dstDiscovery['itemid'];
2332				$item_prototype['hostid'] = $dstDiscovery['hostid'];
2333
2334				if ($item_prototype['valuemapid'] != 0) {
2335					$item_prototype['valuemapid'] = array_key_exists($item_prototype['valuemap']['name'], $valuemap_map)
2336						? $valuemap_map[$item_prototype['valuemap']['name']]
2337						: 0;
2338				}
2339
2340				// map prototype interfaces
2341				if ($dstHost['status'] != HOST_STATUS_TEMPLATE) {
2342					// find a matching interface
2343					$interface = self::findInterfaceForItem($item_prototype['type'], $dstHost['interfaces']);
2344					if ($interface) {
2345						$item_prototype['interfaceid'] = $interface['interfaceid'];
2346					}
2347					// no matching interface found, throw an error
2348					elseif ($interface !== false) {
2349						self::exception(ZBX_API_ERROR_PARAMETERS, _s(
2350							'Cannot find host interface on "%1$s" for item key "%2$s".',
2351							$dstHost['name'],
2352							$item_prototype['key_']
2353						));
2354					}
2355				}
2356
2357				if (!$item_prototype['preprocessing']) {
2358					unset($item_prototype['preprocessing']);
2359				}
2360
2361				if ($item_prototype['type'] == ITEM_TYPE_DEPENDENT) {
2362					$master_itemid = $item_prototype['master_itemid'];
2363
2364					if (array_key_exists($master_itemid, $src_itemid_to_key)) {
2365						$src_item_key = $src_itemid_to_key[$master_itemid];
2366						$item_prototype['master_itemid'] = $itemkey_to_id[$src_item_key];
2367					}
2368					else {
2369						// It's a non-prototype item, so look for it on destination host.
2370						$dst_item = get_same_item_for_host($items[$master_itemid], $dstHost['hostid']);
2371
2372						if (!$dst_item) {
2373							self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot clone item prototypes.'));
2374						}
2375
2376						$item_prototype['master_itemid'] = $dst_item['itemid'];
2377					}
2378				}
2379				else {
2380					unset($item_prototype['master_itemid']);
2381				}
2382
2383				unset($item_prototype['templateid']);
2384				$create_items[] = $item_prototype;
2385			}
2386
2387			if ($create_items) {
2388				$created_itemids = API::ItemPrototype()->create($create_items);
2389
2390				if (!$created_itemids) {
2391					self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot clone item prototypes.'));
2392				}
2393
2394				$new_itemids = array_merge($new_itemids, $created_itemids['itemids']);
2395			}
2396		}
2397
2398		return $new_itemids;
2399	}
2400
2401	/**
2402	 * Copies all of the graphs from the source discovery to the target discovery rule.
2403	 *
2404	 * @throws APIException if graph saving fails
2405	 *
2406	 * @param array $srcDiscovery    The source discovery rule to copy from
2407	 * @param array $dstDiscovery    The target discovery rule to copy to
2408	 *
2409	 * @return array
2410	 */
2411	protected function copyGraphPrototypes(array $srcDiscovery, array $dstDiscovery) {
2412		// fetch source graphs
2413		$srcGraphs = API::GraphPrototype()->get([
2414			'output' => ['graphid', 'name', 'width', 'height', 'yaxismin', 'yaxismax', 'show_work_period',
2415				'show_triggers', 'graphtype', 'show_legend', 'show_3d', 'percent_left', 'percent_right',
2416				'ymin_type', 'ymax_type', 'ymin_itemid', 'ymax_itemid', 'discover'
2417			],
2418			'selectGraphItems' => ['itemid', 'drawtype', 'sortorder', 'color', 'yaxisside', 'calc_fnc', 'type'],
2419			'selectHosts' => ['hostid'],
2420			'discoveryids' => $srcDiscovery['itemid'],
2421			'preservekeys' => true
2422		]);
2423
2424		if (!$srcGraphs) {
2425			return [];
2426		}
2427
2428		$srcItemIds = [];
2429		foreach ($srcGraphs as $key => $graph) {
2430			// skip graphs with items from multiple hosts
2431			if (count($graph['hosts']) > 1) {
2432				unset($srcGraphs[$key]);
2433				continue;
2434			}
2435
2436			// skip graphs with http items
2437			if (httpItemExists($graph['gitems'])) {
2438				unset($srcGraphs[$key]);
2439				continue;
2440			}
2441
2442			// save all used item ids to map them to the new items
2443			foreach ($graph['gitems'] as $item) {
2444				$srcItemIds[$item['itemid']] = $item['itemid'];
2445			}
2446			if ($graph['ymin_itemid']) {
2447				$srcItemIds[$graph['ymin_itemid']] = $graph['ymin_itemid'];
2448			}
2449			if ($graph['ymax_itemid']) {
2450				$srcItemIds[$graph['ymax_itemid']] = $graph['ymax_itemid'];
2451			}
2452		}
2453
2454		// fetch source items
2455		$items = API::Item()->get([
2456			'output' => ['itemid', 'key_'],
2457			'webitems' => true,
2458			'itemids' => $srcItemIds,
2459			'filter' => ['flags' => null],
2460			'preservekeys' => true
2461		]);
2462
2463		$srcItems = [];
2464		$itemKeys = [];
2465		foreach ($items as $item) {
2466			$srcItems[$item['itemid']] = $item;
2467			$itemKeys[$item['key_']] = $item['key_'];
2468		}
2469
2470		// fetch newly cloned items
2471		$newItems = API::Item()->get([
2472			'output' => ['itemid', 'key_'],
2473			'webitems' => true,
2474			'hostids' => $dstDiscovery['hostid'],
2475			'filter' => [
2476				'key_' => $itemKeys,
2477				'flags' => null
2478			],
2479			'preservekeys' => true
2480		]);
2481
2482		$items = array_merge($dstDiscovery['items'], $newItems);
2483		$dstItems = [];
2484		foreach ($items as $item) {
2485			$dstItems[$item['key_']] = $item;
2486		}
2487
2488		$dstGraphs = $srcGraphs;
2489		foreach ($dstGraphs as &$graph) {
2490			unset($graph['graphid']);
2491
2492			foreach ($graph['gitems'] as &$gitem) {
2493				// replace the old item with the new one with the same key
2494				$item = $srcItems[$gitem['itemid']];
2495				$gitem['itemid'] = $dstItems[$item['key_']]['itemid'];
2496			}
2497			unset($gitem);
2498
2499			// replace the old axis items with the new one with the same key
2500			if ($graph['ymin_itemid']) {
2501				$yMinSrcItem = $srcItems[$graph['ymin_itemid']];
2502				$graph['ymin_itemid'] = $dstItems[$yMinSrcItem['key_']]['itemid'];
2503			}
2504			if ($graph['ymax_itemid']) {
2505				$yMaxSrcItem = $srcItems[$graph['ymax_itemid']];
2506				$graph['ymax_itemid'] = $dstItems[$yMaxSrcItem['key_']]['itemid'];
2507			}
2508		}
2509		unset($graph);
2510
2511		// save graphs
2512		$rs = API::GraphPrototype()->create($dstGraphs);
2513		if (!$rs) {
2514			self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot clone graph prototypes.'));
2515		}
2516
2517		return $rs;
2518	}
2519
2520	/**
2521	 * Copies all of the host prototypes from the source discovery to the target
2522	 * discovery rule.
2523	 *
2524	 * @throws APIException if prototype saving fails.
2525	 *
2526	 * @param int   $srcid          The source discovery rule id to copy from.
2527	 * @param array $dstDiscovery   The target discovery rule to copy to.
2528	 *
2529	 * @return array
2530	 */
2531	protected function copyHostPrototypes($srcid, array $dstDiscovery) {
2532		$prototypes = API::HostPrototype()->get([
2533			'discoveryids' => $srcid,
2534			'output' => ['host', 'name', 'status', 'inventory_mode', 'discover', 'custom_interfaces'],
2535			'selectGroupLinks' => ['groupid'],
2536			'selectGroupPrototypes' => ['name'],
2537			'selectInterfaces' => ['type', 'useip', 'ip', 'dns', 'port', 'main', 'details'],
2538			'selectTemplates' => ['templateid'],
2539			'selectTags' => ['tag', 'value'],
2540			'selectMacros' => ['macro', 'type', 'value', 'description'],
2541			'preservekeys' => true
2542		]);
2543
2544		$rs = [];
2545		if ($prototypes) {
2546			foreach ($prototypes as &$prototype) {
2547				$prototype['ruleid'] = $dstDiscovery['itemid'];
2548				unset($prototype['hostid'], $prototype['inventory']['hostid']);
2549
2550				foreach ($prototype['groupLinks'] as &$groupLinks) {
2551					unset($groupLinks['group_prototypeid']);
2552				}
2553				unset($groupLinks);
2554
2555				foreach ($prototype['groupPrototypes'] as &$groupPrototype) {
2556					unset($groupPrototype['group_prototypeid']);
2557				}
2558				unset($groupPrototype);
2559
2560				foreach ($prototype['macros'] as &$macro) {
2561					$macro['type'] = ($macro['type'] == ZBX_MACRO_TYPE_SECRET) ? ZBX_MACRO_TYPE_TEXT : $macro['type'];
2562					$macro += ['value' => ''];
2563				}
2564				unset($macro);
2565			}
2566			unset($prototype);
2567
2568			$rs = API::HostPrototype()->create($prototypes);
2569			if (!$rs) {
2570				self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot clone host prototypes.'));
2571			}
2572		}
2573		return $rs;
2574	}
2575
2576	protected function applyQueryOutputOptions($tableName, $tableAlias, array $options, array $sqlParts) {
2577		$sqlParts = parent::applyQueryOutputOptions($tableName, $tableAlias, $options, $sqlParts);
2578
2579		if ((!$options['countOutput'] && ($this->outputIsRequested('state', $options['output'])
2580				|| $this->outputIsRequested('error', $options['output'])))
2581				|| (is_array($options['search']) && array_key_exists('error', $options['search']))
2582				|| (is_array($options['filter']) && array_key_exists('state', $options['filter']))) {
2583			$sqlParts['left_join'][] = ['alias' => 'ir', 'table' => 'item_rtdata', 'using' => 'itemid'];
2584			$sqlParts['left_table'] = ['alias' => $this->tableAlias, 'table' => $this->tableName];
2585		}
2586
2587		if (!$options['countOutput']) {
2588			if ($this->outputIsRequested('state', $options['output'])) {
2589				$sqlParts = $this->addQuerySelect('ir.state', $sqlParts);
2590			}
2591			if ($this->outputIsRequested('error', $options['output'])) {
2592				/*
2593				 * SQL func COALESCE use for template items because they don't have record
2594				 * in item_rtdata table and DBFetch convert null to '0'
2595				 */
2596				$sqlParts = $this->addQuerySelect(dbConditionCoalesce('ir.error', '', 'error'), $sqlParts);
2597			}
2598
2599			// add filter fields
2600			if ($this->outputIsRequested('formula', $options['selectFilter'])
2601					|| $this->outputIsRequested('eval_formula', $options['selectFilter'])
2602					|| $this->outputIsRequested('conditions', $options['selectFilter'])) {
2603
2604				$sqlParts = $this->addQuerySelect('i.formula', $sqlParts);
2605				$sqlParts = $this->addQuerySelect('i.evaltype', $sqlParts);
2606			}
2607			if ($this->outputIsRequested('evaltype', $options['selectFilter'])) {
2608				$sqlParts = $this->addQuerySelect('i.evaltype', $sqlParts);
2609			}
2610
2611			if ($options['selectHosts'] !== null) {
2612				$sqlParts = $this->addQuerySelect('i.hostid', $sqlParts);
2613			}
2614		}
2615
2616		return $sqlParts;
2617	}
2618
2619	protected function addRelatedObjects(array $options, array $result) {
2620		$result = parent::addRelatedObjects($options, $result);
2621
2622		$itemIds = array_keys($result);
2623
2624		// adding items
2625		if (!is_null($options['selectItems'])) {
2626			if ($options['selectItems'] != API_OUTPUT_COUNT) {
2627				$items = [];
2628				$relationMap = $this->createRelationMap($result, 'parent_itemid', 'itemid', 'item_discovery');
2629				$related_ids = $relationMap->getRelatedIds();
2630
2631				if ($related_ids) {
2632					$items = API::ItemPrototype()->get([
2633						'output' => $options['selectItems'],
2634						'itemids' => $related_ids,
2635						'nopermissions' => true,
2636						'preservekeys' => true
2637					]);
2638				}
2639
2640				$result = $relationMap->mapMany($result, $items, 'items', $options['limitSelects']);
2641			}
2642			else {
2643				$items = API::ItemPrototype()->get([
2644					'discoveryids' => $itemIds,
2645					'nopermissions' => true,
2646					'countOutput' => true,
2647					'groupCount' => true
2648				]);
2649
2650				$items = zbx_toHash($items, 'parent_itemid');
2651				foreach ($result as $itemid => $item) {
2652					$result[$itemid]['items'] = array_key_exists($itemid, $items) ? $items[$itemid]['rowscount'] : '0';
2653				}
2654			}
2655		}
2656
2657		// adding triggers
2658		if (!is_null($options['selectTriggers'])) {
2659			if ($options['selectTriggers'] != API_OUTPUT_COUNT) {
2660				$triggers = [];
2661				$relationMap = new CRelationMap();
2662				$res = DBselect(
2663					'SELECT id.parent_itemid,f.triggerid'.
2664					' FROM item_discovery id,items i,functions f'.
2665					' WHERE '.dbConditionInt('id.parent_itemid', $itemIds).
2666						' AND id.itemid=i.itemid'.
2667						' AND i.itemid=f.itemid'
2668				);
2669				while ($relation = DBfetch($res)) {
2670					$relationMap->addRelation($relation['parent_itemid'], $relation['triggerid']);
2671				}
2672
2673				$related_ids = $relationMap->getRelatedIds();
2674
2675				if ($related_ids) {
2676					$triggers = API::TriggerPrototype()->get([
2677						'output' => $options['selectTriggers'],
2678						'triggerids' => $related_ids,
2679						'preservekeys' => true
2680					]);
2681				}
2682
2683				$result = $relationMap->mapMany($result, $triggers, 'triggers', $options['limitSelects']);
2684			}
2685			else {
2686				$triggers = API::TriggerPrototype()->get([
2687					'discoveryids' => $itemIds,
2688					'countOutput' => true,
2689					'groupCount' => true
2690				]);
2691
2692				$triggers = zbx_toHash($triggers, 'parent_itemid');
2693				foreach ($result as $itemid => $item) {
2694					$result[$itemid]['triggers'] = array_key_exists($itemid, $triggers)
2695						? $triggers[$itemid]['rowscount']
2696						: '0';
2697				}
2698			}
2699		}
2700
2701		// adding graphs
2702		if (!is_null($options['selectGraphs'])) {
2703			if ($options['selectGraphs'] != API_OUTPUT_COUNT) {
2704				$graphs = [];
2705				$relationMap = new CRelationMap();
2706				$res = DBselect(
2707					'SELECT id.parent_itemid,gi.graphid'.
2708					' FROM item_discovery id,items i,graphs_items gi'.
2709					' WHERE '.dbConditionInt('id.parent_itemid', $itemIds).
2710						' AND id.itemid=i.itemid'.
2711						' AND i.itemid=gi.itemid'
2712				);
2713				while ($relation = DBfetch($res)) {
2714					$relationMap->addRelation($relation['parent_itemid'], $relation['graphid']);
2715				}
2716
2717				$related_ids = $relationMap->getRelatedIds();
2718
2719				if ($related_ids) {
2720					$graphs = API::GraphPrototype()->get([
2721						'output' => $options['selectGraphs'],
2722						'graphids' => $related_ids,
2723						'preservekeys' => true
2724					]);
2725				}
2726
2727				$result = $relationMap->mapMany($result, $graphs, 'graphs', $options['limitSelects']);
2728			}
2729			else {
2730				$graphs = API::GraphPrototype()->get([
2731					'discoveryids' => $itemIds,
2732					'countOutput' => true,
2733					'groupCount' => true
2734				]);
2735
2736				$graphs = zbx_toHash($graphs, 'parent_itemid');
2737				foreach ($result as $itemid => $item) {
2738					$result[$itemid]['graphs'] = array_key_exists($itemid, $graphs)
2739						? $graphs[$itemid]['rowscount']
2740						: '0';
2741				}
2742			}
2743		}
2744
2745		// adding hosts
2746		if ($options['selectHostPrototypes'] !== null) {
2747			if ($options['selectHostPrototypes'] != API_OUTPUT_COUNT) {
2748				$hostPrototypes = [];
2749				$relationMap = $this->createRelationMap($result, 'parent_itemid', 'hostid', 'host_discovery');
2750				$related_ids = $relationMap->getRelatedIds();
2751
2752				if ($related_ids) {
2753					$hostPrototypes = API::HostPrototype()->get([
2754						'output' => $options['selectHostPrototypes'],
2755						'hostids' => $related_ids,
2756						'nopermissions' => true,
2757						'preservekeys' => true
2758					]);
2759				}
2760
2761				$result = $relationMap->mapMany($result, $hostPrototypes, 'hostPrototypes', $options['limitSelects']);
2762			}
2763			else {
2764				$hostPrototypes = API::HostPrototype()->get([
2765					'discoveryids' => $itemIds,
2766					'nopermissions' => true,
2767					'countOutput' => true,
2768					'groupCount' => true
2769				]);
2770				$hostPrototypes = zbx_toHash($hostPrototypes, 'parent_itemid');
2771
2772				foreach ($result as $itemid => $item) {
2773					$result[$itemid]['hostPrototypes'] = array_key_exists($itemid, $hostPrototypes)
2774						? $hostPrototypes[$itemid]['rowscount']
2775						: '0';
2776				}
2777			}
2778		}
2779
2780		if ($options['selectFilter'] !== null) {
2781			$formulaRequested = $this->outputIsRequested('formula', $options['selectFilter']);
2782			$evalFormulaRequested = $this->outputIsRequested('eval_formula', $options['selectFilter']);
2783			$conditionsRequested = $this->outputIsRequested('conditions', $options['selectFilter']);
2784
2785			$filters = [];
2786			foreach ($result as $rule) {
2787				$filters[$rule['itemid']] = [
2788					'evaltype' => $rule['evaltype'],
2789					'formula' => isset($rule['formula']) ? $rule['formula'] : ''
2790				];
2791			}
2792
2793			// adding conditions
2794			if ($formulaRequested || $evalFormulaRequested || $conditionsRequested) {
2795				$conditions = DB::select('item_condition', [
2796					'output' => ['item_conditionid', 'macro', 'value', 'itemid', 'operator'],
2797					'filter' => ['itemid' => $itemIds],
2798					'preservekeys' => true,
2799					'sortfield' => ['item_conditionid']
2800				]);
2801				$relationMap = $this->createRelationMap($conditions, 'itemid', 'item_conditionid');
2802
2803				$filters = $relationMap->mapMany($filters, $conditions, 'conditions');
2804
2805				foreach ($filters as &$filter) {
2806					// in case of a custom expression - use the given formula
2807					if ($filter['evaltype'] == CONDITION_EVAL_TYPE_EXPRESSION) {
2808						$formula = $filter['formula'];
2809					}
2810					// in other cases - generate the formula automatically
2811					else {
2812						// sort the conditions by macro before generating the formula
2813						$conditions = zbx_toHash($filter['conditions'], 'item_conditionid');
2814						$conditions = order_macros($conditions, 'macro');
2815
2816						$formulaConditions = [];
2817						foreach ($conditions as $condition) {
2818							$formulaConditions[$condition['item_conditionid']] = $condition['macro'];
2819						}
2820						$formula = CConditionHelper::getFormula($formulaConditions, $filter['evaltype']);
2821					}
2822
2823					// generate formulaids from the effective formula
2824					$formulaIds = CConditionHelper::getFormulaIds($formula);
2825					foreach ($filter['conditions'] as &$condition) {
2826						$condition['formulaid'] = $formulaIds[$condition['item_conditionid']];
2827					}
2828					unset($condition);
2829
2830					// generated a letter based formula only for rules with custom expressions
2831					if ($formulaRequested && $filter['evaltype'] == CONDITION_EVAL_TYPE_EXPRESSION) {
2832						$filter['formula'] = CConditionHelper::replaceNumericIds($formula, $formulaIds);
2833					}
2834
2835					if ($evalFormulaRequested) {
2836						$filter['eval_formula'] = CConditionHelper::replaceNumericIds($formula, $formulaIds);
2837					}
2838				}
2839				unset($filter);
2840			}
2841
2842			// add filters to the result
2843			foreach ($result as &$rule) {
2844				$rule['filter'] = $filters[$rule['itemid']];
2845			}
2846			unset($rule);
2847		}
2848
2849		// Add LLD macro paths.
2850		if ($options['selectLLDMacroPaths'] !== null && $options['selectLLDMacroPaths'] != API_OUTPUT_COUNT) {
2851			$lld_macro_paths = API::getApiService()->select('lld_macro_path', [
2852				'output' => $this->outputExtend($options['selectLLDMacroPaths'], ['itemid', 'lld_macro_pathid']),
2853				'filter' => ['itemid' => $itemIds]
2854			]);
2855
2856			foreach ($result as &$lld_macro_path) {
2857				$lld_macro_path['lld_macro_paths'] = [];
2858			}
2859			unset($lld_macro_path);
2860
2861			foreach ($lld_macro_paths as $lld_macro_path) {
2862				$itemid = $lld_macro_path['itemid'];
2863
2864				if (!$this->outputIsRequested('lld_macro_pathid', $options['selectLLDMacroPaths'])) {
2865					unset($lld_macro_path['lld_macro_pathid']);
2866				}
2867				unset($lld_macro_path['itemid']);
2868
2869				$result[$itemid]['lld_macro_paths'][] = $lld_macro_path;
2870			}
2871		}
2872
2873		// add overrides
2874		if ($options['selectOverrides'] !== null && $options['selectOverrides'] != API_OUTPUT_COUNT) {
2875			$ovrd_fields = ['itemid', 'lld_overrideid'];
2876			$filter_requested = $this->outputIsRequested('filter', $options['selectOverrides']);
2877			$operations_requested = $this->outputIsRequested('operations', $options['selectOverrides']);
2878
2879			if ($filter_requested) {
2880				$ovrd_fields = array_merge($ovrd_fields, ['formula', 'evaltype']);
2881			}
2882
2883			$overrides = API::getApiService()->select('lld_override', [
2884				'output' => $this->outputExtend($options['selectOverrides'], $ovrd_fields),
2885				'filter' => ['itemid' => $itemIds],
2886				'preservekeys' => true
2887			]);
2888
2889			if ($filter_requested && $overrides) {
2890				$conditions = DB::select('lld_override_condition', [
2891					'output' => ['lld_override_conditionid', 'macro', 'value', 'lld_overrideid', 'operator'],
2892					'filter' => ['lld_overrideid' => array_keys($overrides)],
2893					'sortfield' => ['lld_override_conditionid'],
2894					'preservekeys' => true
2895				]);
2896
2897				$relation_map = $this->createRelationMap($conditions, 'lld_overrideid', 'lld_override_conditionid');
2898
2899				foreach ($overrides as &$override) {
2900					$override['filter'] = [
2901						'evaltype' => $override['evaltype'],
2902						'formula' => $override['formula']
2903					];
2904					unset($override['evaltype'], $override['formula']);
2905				}
2906				unset($override);
2907
2908				$overrides = $relation_map->mapMany($overrides, $conditions, 'conditions');
2909
2910				foreach ($overrides as &$override) {
2911					$override['filter'] += ['conditions' => $override['conditions']];
2912					unset($override['conditions']);
2913
2914					if ($override['filter']['evaltype'] == CONDITION_EVAL_TYPE_EXPRESSION) {
2915						$formula = $override['filter']['formula'];
2916					}
2917					else {
2918						$conditions = zbx_toHash($override['filter']['conditions'], 'lld_override_conditionid');
2919						$conditions = order_macros($conditions, 'macro');
2920						$formula_conditions = [];
2921
2922						foreach ($conditions as $condition) {
2923							$formula_conditions[$condition['lld_override_conditionid']] = $condition['macro'];
2924						}
2925
2926						$formula = CConditionHelper::getFormula($formula_conditions, $override['filter']['evaltype']);
2927					}
2928
2929					$formulaids = CConditionHelper::getFormulaIds($formula);
2930
2931					foreach ($override['filter']['conditions'] as &$condition) {
2932						$condition['formulaid'] = $formulaids[$condition['lld_override_conditionid']];
2933						unset($condition['lld_override_conditionid'], $condition['lld_overrideid']);
2934					}
2935					unset($condition);
2936
2937					if ($override['filter']['evaltype'] == CONDITION_EVAL_TYPE_EXPRESSION) {
2938						$override['filter']['formula'] = CConditionHelper::replaceNumericIds($formula, $formulaids);
2939						$override['filter']['eval_formula'] = $override['filter']['formula'];
2940					}
2941					else {
2942						$override['filter']['eval_formula'] = CConditionHelper::replaceNumericIds($formula,
2943							$formulaids
2944						);
2945					}
2946				}
2947				unset($override);
2948			}
2949
2950			if ($operations_requested && $overrides) {
2951				$operations = DB::select('lld_override_operation', [
2952					'output' => ['lld_override_operationid', 'lld_overrideid', 'operationobject', 'operator', 'value'],
2953					'filter' => ['lld_overrideid' => array_keys($overrides)],
2954					'sortfield' => ['lld_override_operationid'],
2955					'preservekeys' => true
2956				]);
2957
2958				if ($operations) {
2959					$opdiscover = DB::select('lld_override_opdiscover', [
2960						'output' => ['lld_override_operationid', 'discover'],
2961						'filter' => ['lld_override_operationid' => array_keys($operations)]
2962					]);
2963
2964					$item_prototype_objectids = [];
2965					$trigger_prototype_objectids = [];
2966					$host_prototype_objectids = [];
2967
2968					foreach ($operations as $operation) {
2969						switch ($operation['operationobject']) {
2970							case OPERATION_OBJECT_ITEM_PROTOTYPE:
2971								$item_prototype_objectids[$operation['lld_override_operationid']] = true;
2972								break;
2973
2974							case OPERATION_OBJECT_TRIGGER_PROTOTYPE:
2975								$trigger_prototype_objectids[$operation['lld_override_operationid']] = true;
2976								break;
2977
2978							case OPERATION_OBJECT_HOST_PROTOTYPE:
2979								$host_prototype_objectids[$operation['lld_override_operationid']] = true;
2980								break;
2981						}
2982					}
2983
2984					if ($item_prototype_objectids || $trigger_prototype_objectids || $host_prototype_objectids) {
2985						$opstatus = DB::select('lld_override_opstatus', [
2986							'output' => ['lld_override_operationid', 'status'],
2987							'filter' => ['lld_override_operationid' => array_keys(
2988								$item_prototype_objectids + $trigger_prototype_objectids + $host_prototype_objectids
2989							)]
2990						]);
2991					}
2992
2993					if ($item_prototype_objectids) {
2994						$ophistory = DB::select('lld_override_ophistory', [
2995							'output' => ['lld_override_operationid', 'history'],
2996							'filter' => ['lld_override_operationid' => array_keys($item_prototype_objectids)]
2997						]);
2998						$optrends = DB::select('lld_override_optrends', [
2999							'output' => ['lld_override_operationid', 'trends'],
3000							'filter' => ['lld_override_operationid' => array_keys($item_prototype_objectids)]
3001						]);
3002						$opperiod = DB::select('lld_override_opperiod', [
3003							'output' => ['lld_override_operationid', 'delay'],
3004							'filter' => ['lld_override_operationid' => array_keys($item_prototype_objectids)]
3005						]);
3006					}
3007
3008					if ($trigger_prototype_objectids) {
3009						$opseverity = DB::select('lld_override_opseverity', [
3010							'output' => ['lld_override_operationid', 'severity'],
3011							'filter' => ['lld_override_operationid' => array_keys($trigger_prototype_objectids)]
3012						]);
3013					}
3014
3015					if ($trigger_prototype_objectids || $host_prototype_objectids || $item_prototype_objectids) {
3016						$optag = DB::select('lld_override_optag', [
3017							'output' => ['lld_override_operationid', 'tag', 'value'],
3018							'filter' => ['lld_override_operationid' => array_keys(
3019								$trigger_prototype_objectids + $host_prototype_objectids + $item_prototype_objectids
3020							)]
3021						]);
3022					}
3023
3024					if ($host_prototype_objectids) {
3025						$optemplate = DB::select('lld_override_optemplate', [
3026							'output' => ['lld_override_operationid', 'templateid'],
3027							'filter' => ['lld_override_operationid' => array_keys($host_prototype_objectids)]
3028						]);
3029						$opinventory = DB::select('lld_override_opinventory', [
3030							'output' => ['lld_override_operationid', 'inventory_mode'],
3031							'filter' => ['lld_override_operationid' => array_keys($host_prototype_objectids)]
3032						]);
3033					}
3034
3035					foreach ($operations as &$operation) {
3036						$lld_override_operationid = $operation['lld_override_operationid'];
3037
3038						if ($item_prototype_objectids || $trigger_prototype_objectids || $host_prototype_objectids) {
3039							foreach ($opstatus as $row) {
3040								if (bccomp($lld_override_operationid, $row['lld_override_operationid']) == 0) {
3041									$operation['opstatus']['status'] = $row['status'];
3042								}
3043							}
3044						}
3045
3046						foreach ($opdiscover as $row) {
3047							if (bccomp($lld_override_operationid, $row['lld_override_operationid']) == 0) {
3048								$operation['opdiscover']['discover'] = $row['discover'];
3049							}
3050						}
3051
3052						if ($item_prototype_objectids) {
3053							foreach ($ophistory as $row) {
3054								if (bccomp($lld_override_operationid, $row['lld_override_operationid']) == 0) {
3055									$operation['ophistory']['history'] = $row['history'];
3056								}
3057							}
3058
3059							foreach ($optrends as $row) {
3060								if (bccomp($lld_override_operationid, $row['lld_override_operationid']) == 0) {
3061									$operation['optrends']['trends'] = $row['trends'];
3062								}
3063							}
3064
3065							foreach ($opperiod as $row) {
3066								if (bccomp($lld_override_operationid, $row['lld_override_operationid']) == 0) {
3067									$operation['opperiod']['delay'] = $row['delay'];
3068								}
3069							}
3070						}
3071
3072						if ($trigger_prototype_objectids) {
3073							foreach ($opseverity as $row) {
3074								if (bccomp($lld_override_operationid, $row['lld_override_operationid']) == 0) {
3075									$operation['opseverity']['severity'] = $row['severity'];
3076								}
3077							}
3078						}
3079
3080						if ($trigger_prototype_objectids || $host_prototype_objectids || $item_prototype_objectids) {
3081							foreach ($optag as $row) {
3082								if (bccomp($lld_override_operationid, $row['lld_override_operationid']) == 0) {
3083									$operation['optag'][] = ['tag' => $row['tag'], 'value' => $row['value']];
3084								}
3085							}
3086						}
3087
3088						if ($host_prototype_objectids) {
3089							foreach ($optemplate as $row) {
3090								if (bccomp($lld_override_operationid, $row['lld_override_operationid']) == 0) {
3091									$operation['optemplate'][] = ['templateid' => $row['templateid']];
3092								}
3093							}
3094
3095							foreach ($opinventory as $row) {
3096								if (bccomp($lld_override_operationid, $row['lld_override_operationid']) == 0) {
3097									$operation['opinventory']['inventory_mode'] = $row['inventory_mode'];
3098								}
3099							}
3100						}
3101					}
3102					unset($operation);
3103				}
3104
3105				$relation_map = $this->createRelationMap($operations, 'lld_overrideid', 'lld_override_operationid');
3106
3107				$overrides = $relation_map->mapMany($overrides, $operations, 'operations');
3108			}
3109
3110			foreach ($result as &$row) {
3111				$row['overrides'] = [];
3112
3113				foreach ($overrides as $override) {
3114					if (bccomp($override['itemid'], $row['itemid']) == 0) {
3115						unset($override['itemid'], $override['lld_overrideid']);
3116
3117						if ($operations_requested) {
3118							foreach ($override['operations'] as &$operation) {
3119								unset($operation['lld_override_operationid'], $operation['lld_overrideid']);
3120							}
3121							unset($operation);
3122						}
3123
3124						$row['overrides'][] = $override;
3125					}
3126				}
3127			}
3128			unset($row);
3129		}
3130
3131		return $result;
3132	}
3133}
3134